// 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, command_buffer_allocator: Arc, rcx: Option, } 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(); } }