mirror of
https://github.com/vulkano-rs/vulkano.git
synced 2024-11-25 08:14:20 +00:00
f6bc05df94
* Update dependencies * fmt
393 lines
13 KiB
Rust
393 lines
13 KiB
Rust
// This is an example demonstrating an application with some more non-trivial functionality.
|
|
// It should get you more up to speed with how you can use Vulkano.
|
|
//
|
|
// It contains:
|
|
//
|
|
// - A compute pipeline to calculate Mandelbrot and Julia fractals writing them to an image.
|
|
// - A graphics pipeline to draw the fractal image over a quad that covers the whole screen.
|
|
// - A renderpass rendering that image on the swapchain image.
|
|
// - An organized renderer with functionality good enough to copy to other projects.
|
|
// - A simple `FractalApp` to handle runtime state.
|
|
// - A simple `InputState` to interact with the application.
|
|
|
|
use fractal_compute_pipeline::FractalComputePipeline;
|
|
use glam::Vec2;
|
|
use input::InputState;
|
|
use place_over_frame::RenderPassPlaceOverFrame;
|
|
use std::{
|
|
error::Error,
|
|
sync::Arc,
|
|
time::{Duration, Instant},
|
|
};
|
|
use vulkano::{
|
|
command_buffer::allocator::{
|
|
StandardCommandBufferAllocator, StandardCommandBufferAllocatorCreateInfo,
|
|
},
|
|
descriptor_set::allocator::StandardDescriptorSetAllocator,
|
|
image::ImageUsage,
|
|
swapchain::PresentMode,
|
|
sync::GpuFuture,
|
|
};
|
|
use vulkano_util::{
|
|
context::{VulkanoConfig, VulkanoContext},
|
|
renderer::{VulkanoWindowRenderer, DEFAULT_IMAGE_FORMAT},
|
|
window::{VulkanoWindows, WindowDescriptor},
|
|
};
|
|
use winit::{
|
|
application::ApplicationHandler,
|
|
event::WindowEvent,
|
|
event_loop::{ActiveEventLoop, EventLoop},
|
|
window::{Fullscreen, WindowId},
|
|
};
|
|
|
|
mod fractal_compute_pipeline;
|
|
mod input;
|
|
mod pixels_draw_pipeline;
|
|
mod place_over_frame;
|
|
|
|
const MAX_ITERS_INIT: u32 = 200;
|
|
const MOVE_SPEED: f32 = 0.5;
|
|
|
|
fn main() -> Result<(), impl Error> {
|
|
// Create the event loop.
|
|
let event_loop = EventLoop::new().unwrap();
|
|
let mut app = App::new(&event_loop);
|
|
|
|
println!(
|
|
"\
|
|
Usage:
|
|
WASD: Pan view
|
|
Scroll: Zoom in/out
|
|
Space: Toggle between Mandelbrot and Julia
|
|
Enter: Randomize color palette
|
|
Equals/Minus: Increase/Decrease max iterations
|
|
F: Toggle full-screen
|
|
Right mouse: Stop movement in Julia (mouse position determines c)
|
|
Esc: Quit\
|
|
",
|
|
);
|
|
|
|
event_loop.run_app(&mut app)
|
|
}
|
|
|
|
struct App {
|
|
context: VulkanoContext,
|
|
windows: VulkanoWindows,
|
|
descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
|
|
command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
|
|
rcx: Option<RenderContext>,
|
|
}
|
|
|
|
struct RenderContext {
|
|
/// Pipeline that computes Mandelbrot & Julia fractals and writes them to an image.
|
|
fractal_pipeline: FractalComputePipeline,
|
|
/// Our render pipeline (pass).
|
|
place_over_frame: RenderPassPlaceOverFrame,
|
|
/// Toggle that flips between Julia and Mandelbrot.
|
|
is_julia: bool,
|
|
/// Toggle that stops the movement on Julia.
|
|
is_c_paused: bool,
|
|
/// C is a constant input to Julia escape time algorithm (mouse position).
|
|
c: Vec2,
|
|
/// Our zoom level.
|
|
scale: Vec2,
|
|
/// Our translation on the complex plane.
|
|
translation: Vec2,
|
|
/// How long the escape time algorithm should run (higher = less performance, more accurate
|
|
/// image).
|
|
max_iters: u32,
|
|
/// Time tracking, useful for frame independent movement.
|
|
time: Instant,
|
|
dt: f32,
|
|
dt_sum: f32,
|
|
frame_count: f32,
|
|
avg_fps: f32,
|
|
/// Input state to handle mouse positions, continuous movement etc.
|
|
input_state: InputState,
|
|
render_target_id: usize,
|
|
}
|
|
|
|
impl App {
|
|
fn new(_event_loop: &EventLoop<()>) -> Self {
|
|
let context = VulkanoContext::new(VulkanoConfig::default());
|
|
let windows = VulkanoWindows::default();
|
|
|
|
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
|
|
context.device().clone(),
|
|
Default::default(),
|
|
));
|
|
let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
|
|
context.device().clone(),
|
|
StandardCommandBufferAllocatorCreateInfo {
|
|
secondary_buffer_count: 32,
|
|
..Default::default()
|
|
},
|
|
));
|
|
|
|
App {
|
|
context,
|
|
windows,
|
|
descriptor_set_allocator,
|
|
command_buffer_allocator,
|
|
rcx: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ApplicationHandler for App {
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
let _id = self.windows.create_window(
|
|
event_loop,
|
|
&self.context,
|
|
&WindowDescriptor {
|
|
title: "Fractal".to_string(),
|
|
present_mode: PresentMode::Fifo,
|
|
..Default::default()
|
|
},
|
|
|_| {},
|
|
);
|
|
|
|
// Add our render target image onto which we'll be rendering our fractals.
|
|
let render_target_id = 0;
|
|
let window_renderer = self.windows.get_primary_renderer_mut().unwrap();
|
|
|
|
// Make sure the image usage is correct (based on your pipeline).
|
|
window_renderer.add_additional_image_view(
|
|
render_target_id,
|
|
DEFAULT_IMAGE_FORMAT,
|
|
ImageUsage::SAMPLED | ImageUsage::STORAGE | ImageUsage::TRANSFER_DST,
|
|
);
|
|
|
|
let gfx_queue = self.context.graphics_queue();
|
|
|
|
self.rcx = Some(RenderContext {
|
|
render_target_id,
|
|
fractal_pipeline: FractalComputePipeline::new(
|
|
gfx_queue.clone(),
|
|
self.context.memory_allocator().clone(),
|
|
self.command_buffer_allocator.clone(),
|
|
self.descriptor_set_allocator.clone(),
|
|
),
|
|
place_over_frame: RenderPassPlaceOverFrame::new(
|
|
gfx_queue.clone(),
|
|
self.command_buffer_allocator.clone(),
|
|
self.descriptor_set_allocator.clone(),
|
|
window_renderer.swapchain_format(),
|
|
window_renderer.swapchain_image_views(),
|
|
),
|
|
is_julia: false,
|
|
is_c_paused: false,
|
|
c: Vec2::new(0.0, 0.0),
|
|
scale: Vec2::new(4.0, 4.0),
|
|
translation: Vec2::new(0.0, 0.0),
|
|
max_iters: MAX_ITERS_INIT,
|
|
time: Instant::now(),
|
|
dt: 0.0,
|
|
dt_sum: 0.0,
|
|
frame_count: 0.0,
|
|
avg_fps: 0.0,
|
|
input_state: InputState::new(),
|
|
});
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
_window_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
let renderer = self.windows.get_primary_renderer_mut().unwrap();
|
|
let rcx = self.rcx.as_mut().unwrap();
|
|
let window_size = renderer.window().inner_size();
|
|
|
|
match event {
|
|
WindowEvent::CloseRequested => {
|
|
event_loop.exit();
|
|
}
|
|
WindowEvent::Resized(..) | WindowEvent::ScaleFactorChanged { .. } => {
|
|
renderer.resize();
|
|
}
|
|
WindowEvent::RedrawRequested => {
|
|
// Tasks for redrawing:
|
|
// 1. Update state based on events
|
|
// 2. Compute & Render
|
|
// 3. Reset input state
|
|
// 4. Update time & title
|
|
|
|
// Skip this frame when minimized.
|
|
if window_size.width == 0 || window_size.height == 0 {
|
|
return;
|
|
}
|
|
|
|
rcx.update_state_after_inputs(renderer);
|
|
|
|
// Start the frame.
|
|
let before_pipeline_future = match renderer.acquire(
|
|
Some(Duration::from_millis(1000)),
|
|
|swapchain_image_views| {
|
|
rcx.place_over_frame
|
|
.recreate_framebuffers(swapchain_image_views)
|
|
},
|
|
) {
|
|
Err(e) => {
|
|
println!("{e}");
|
|
return;
|
|
}
|
|
Ok(future) => future,
|
|
};
|
|
|
|
// Retrieve the target image.
|
|
let image = renderer.get_additional_image_view(rcx.render_target_id);
|
|
|
|
// Compute our fractal (writes to target image). Join future with
|
|
// `before_pipeline_future`.
|
|
let after_compute = rcx
|
|
.fractal_pipeline
|
|
.compute(
|
|
image.clone(),
|
|
rcx.c,
|
|
rcx.scale,
|
|
rcx.translation,
|
|
rcx.max_iters,
|
|
rcx.is_julia,
|
|
)
|
|
.join(before_pipeline_future);
|
|
|
|
// Render the image over the swapchain image, inputting the previous future.
|
|
let after_renderpass_future = rcx.place_over_frame.render(
|
|
after_compute,
|
|
image,
|
|
renderer.swapchain_image_view(),
|
|
renderer.image_index(),
|
|
);
|
|
|
|
// Finish the frame (which presents the view), inputting the last future. Wait for
|
|
// the future so resources are not in use when we render.
|
|
renderer.present(after_renderpass_future, true);
|
|
|
|
rcx.input_state.reset();
|
|
rcx.update_time();
|
|
renderer.window().set_title(&format!(
|
|
"{} fps: {:.2} dt: {:.2}, Max Iterations: {}",
|
|
if rcx.is_julia { "Julia" } else { "Mandelbrot" },
|
|
rcx.avg_fps(),
|
|
rcx.dt(),
|
|
rcx.max_iters
|
|
));
|
|
}
|
|
_ => {
|
|
// Pass event for the app to handle our inputs.
|
|
rcx.input_state.handle_input(window_size, &event);
|
|
}
|
|
}
|
|
|
|
if rcx.input_state.should_quit {
|
|
event_loop.exit();
|
|
}
|
|
}
|
|
|
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
|
let window_renderer = self.windows.get_primary_renderer_mut().unwrap();
|
|
window_renderer.window().request_redraw();
|
|
}
|
|
}
|
|
|
|
impl RenderContext {
|
|
/// Updates app state based on input state.
|
|
fn update_state_after_inputs(&mut self, renderer: &mut VulkanoWindowRenderer) {
|
|
// Zoom in or out.
|
|
if self.input_state.scroll_delta > 0. {
|
|
self.scale /= 1.05;
|
|
} else if self.input_state.scroll_delta < 0. {
|
|
self.scale *= 1.05;
|
|
}
|
|
|
|
// Move speed scaled by zoom level.
|
|
let move_speed = MOVE_SPEED * self.dt * self.scale.x;
|
|
|
|
// Panning.
|
|
if self.input_state.pan_up {
|
|
self.translation += Vec2::new(0.0, move_speed);
|
|
}
|
|
if self.input_state.pan_down {
|
|
self.translation += Vec2::new(0.0, -move_speed);
|
|
}
|
|
if self.input_state.pan_right {
|
|
self.translation += Vec2::new(move_speed, 0.0);
|
|
}
|
|
if self.input_state.pan_left {
|
|
self.translation += Vec2::new(-move_speed, 0.0);
|
|
}
|
|
|
|
// Toggle between Julia and Mandelbrot.
|
|
if self.input_state.toggle_julia {
|
|
self.is_julia = !self.is_julia;
|
|
}
|
|
|
|
// Toggle c.
|
|
if self.input_state.toggle_c {
|
|
self.is_c_paused = !self.is_c_paused;
|
|
}
|
|
|
|
// Update c.
|
|
if !self.is_c_paused {
|
|
// Scale normalized mouse pos between -1.0 and 1.0.
|
|
let mouse_pos = self.input_state.normalized_mouse_pos() * 2.0 - Vec2::new(1.0, 1.0);
|
|
// Scale by our zoom (scale) level so when zooming in the movement on Julia is not so
|
|
// drastic.
|
|
self.c = mouse_pos * self.scale.x;
|
|
}
|
|
|
|
// Update how many iterations we have.
|
|
if self.input_state.increase_iterations {
|
|
self.max_iters += 1;
|
|
}
|
|
if self.input_state.decrease_iterations {
|
|
if self.max_iters as i32 - 1 <= 0 {
|
|
self.max_iters = 0;
|
|
} else {
|
|
self.max_iters -= 1;
|
|
}
|
|
}
|
|
|
|
// Randomize our palette.
|
|
if self.input_state.randomize_palette {
|
|
self.fractal_pipeline.randomize_palette();
|
|
}
|
|
|
|
// Toggle full-screen.
|
|
if self.input_state.toggle_full_screen {
|
|
let is_full_screen = renderer.window().fullscreen().is_some();
|
|
renderer.window().set_fullscreen(if !is_full_screen {
|
|
Some(Fullscreen::Borderless(renderer.window().current_monitor()))
|
|
} else {
|
|
None
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Returns the average FPS.
|
|
fn avg_fps(&self) -> f32 {
|
|
self.avg_fps
|
|
}
|
|
|
|
/// Returns the delta time in milliseconds.
|
|
fn dt(&self) -> f32 {
|
|
self.dt * 1000.0
|
|
}
|
|
|
|
/// Updates times and dt at the end of each frame.
|
|
fn update_time(&mut self) {
|
|
// Each second, update average fps & reset frame count & dt sum.
|
|
if self.dt_sum > 1.0 {
|
|
self.avg_fps = self.frame_count / self.dt_sum;
|
|
self.frame_count = 0.0;
|
|
self.dt_sum = 0.0;
|
|
}
|
|
self.dt = self.time.elapsed().as_secs_f32();
|
|
self.dt_sum += self.dt;
|
|
self.frame_count += 1.0;
|
|
self.time = Instant::now();
|
|
}
|
|
}
|