diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 1ba5349..f1e8508 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -50,19 +50,10 @@ impl ApplicationHandler for App { event_loop.exit(); }, WindowEvent::RedrawRequested => { - // Redraw the application. - // - // It's preferable for applications that do not render continuously to render in - // this event rather than in AboutToWait, since rendering in here allows - // the program to gracefully handle redraws requested by the OS. - - // Draw. - - // Queue a RedrawRequested event. - // - // You only need to call this if you've determined that you need to redraw in - // applications which do not always need to. Applications that redraw continuously - // can render here instead. + if let Some(renderer) = self.renderer.as_mut() { + renderer.draw_frame().expect("Failed to draw frame"); + } + self.window.as_ref().unwrap().request_redraw(); } _ => (), diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index bde5581..82f112f 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -43,14 +43,18 @@ pub struct Renderer { swapchain_image_views: Vec, /// The pool used to allocate command buffers. command_pool: vk::CommandPool, - /// The buffer used to record GPU commands. - command_buffer: vk::CommandBuffer, - /// Signaled when the swapchain has provided an image to render into. - image_available_semaphore: vk::Semaphore, - /// Signaled when rendering is complete and the image is ready for presentation. - render_finished_semaphore: vk::Semaphore, - /// Signaled when the GPU has finished executing the command buffer. - in_flight_fence: vk::Fence, + /// The buffers used to record GPU commands (one per frame in flight). + command_buffers: Vec, + /// Semaphores signaled when an image is acquired (one per frame in flight). + image_available_semaphores: Vec, + /// Semaphores signaled when rendering is complete (one per frame in flight). + render_finished_semaphores: Vec, + /// Fences signaled when the GPU has finished a frame (one per frame in flight). + in_flight_fences: Vec, + /// Tracks which frame is using which swapchain image (one per swapchain image). + images_in_flight: Vec, + /// The index of the frame currently being processed (0..MAX_FRAMES_IN_FLIGHT). + current_frame: usize, } impl Renderer { @@ -146,22 +150,41 @@ impl Renderer { let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? }; - // Allocate the main command buffer used for rendering - let alloc_info = vk::CommandBufferAllocateInfo::default() - .command_pool(command_pool) - .level(vk::CommandBufferLevel::PRIMARY) - .command_buffer_count(1); + // Create synchronization primitives + // We use MAX_FRAMES_IN_FLIGHT (2) to allow the CPU to stay one frame ahead of the GPU. + const MAX_FRAMES_IN_FLIGHT: usize = 2; + let mut image_available_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + let mut render_finished_semaphores = Vec::with_capacity(swapchain_images.len()); + let mut in_flight_fences = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + let mut command_buffers = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); - let command_buffer = unsafe { device.allocate_command_buffers(&alloc_info)?[0] }; - - // Create semaphores and fence for synchronization let semaphore_info = vk::SemaphoreCreateInfo::default(); let fence_info = vk::FenceCreateInfo::default() .flags(vk::FenceCreateFlags::SIGNALED); - let image_available_semaphore = unsafe { device.create_semaphore(&semaphore_info, None)? }; - let render_finished_semaphore = unsafe { device.create_semaphore(&semaphore_info, None)? }; - let in_flight_fence = unsafe { device.create_fence(&fence_info, None)? }; + let alloc_info = vk::CommandBufferAllocateInfo::default() + .command_pool(command_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(MAX_FRAMES_IN_FLIGHT as u32); + + let cbs = unsafe { device.allocate_command_buffers(&alloc_info)? }; + + for i in 0..MAX_FRAMES_IN_FLIGHT { + image_available_semaphores.push(unsafe { device.create_semaphore(&semaphore_info, None)? }); + in_flight_fences.push(unsafe { device.create_fence(&fence_info, None)? }); + command_buffers.push(cbs[i]); + } + + // We create one render-finished semaphore per swapchain image. + // This satisfies strict drivers (like AMD RADV) that require a semaphore + // to be tied to a specific image until it is re-acquired. + for _ in 0..swapchain_images.len() { + render_finished_semaphores.push(unsafe { device.create_semaphore(&semaphore_info, None)? }); + } + + // Initially, no image is in use by a frame. + // We use this to track which frame's fence is protecting which swapchain image. + let images_in_flight = vec![vk::Fence::null(); swapchain_images.len()]; Ok(Self { _entry: entry, @@ -181,10 +204,12 @@ impl Renderer { swapchain_extent, swapchain_image_views: image_views, command_pool, - command_buffer, - image_available_semaphore, - render_finished_semaphore, - in_flight_fence, + command_buffers, + image_available_semaphores, + render_finished_semaphores, + in_flight_fences, + images_in_flight, + current_frame: 0, }) } @@ -389,18 +414,170 @@ impl Renderer { Ok(views) } + + /// Renders a single frame. + /// + /// This function handles synchronization, image acquisition, command recording + /// for clearing the screen, and presentation. + pub fn draw_frame(&mut self) -> Result<(), RendererError> { + let in_flight_fence = self.in_flight_fences[self.current_frame]; + let image_available_semaphore = self.image_available_semaphores[self.current_frame]; + let cmd = self.command_buffers[self.current_frame]; + + // 1. Wait for the current frame's GPU work to finish + unsafe { + self.device.wait_for_fences(&[in_flight_fence], true, u64::MAX)?; + self.device.reset_fences(&[in_flight_fence])?; + } + + // 2. Acquire an image from the swapchain + let (image_index, _is_suboptimal) = unsafe { + self.swapchain_loader.acquire_next_image( + self.swapchain, + u64::MAX, + image_available_semaphore, + vk::Fence::null(), + )? + }; + + // If the acquired image is still being used by a previous frame, wait for it + let image_fence = self.images_in_flight[image_index as usize]; + if image_fence != vk::Fence::null() { + unsafe { self.device.wait_for_fences(&[image_fence], true, u64::MAX)? }; + } + // Mark the image as being in use by the current frame's fence + self.images_in_flight[image_index as usize] = in_flight_fence; + + // Use the semaphore tied to this specific swapchain image for rendering completion + let render_finished_semaphore = self.render_finished_semaphores[image_index as usize]; + + // 3. Reset and begin recording the command buffer + unsafe { + self.device.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())?; + let begin_info = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + self.device.begin_command_buffer(cmd, &begin_info)?; + } + + let image = self.swapchain_images[image_index as usize]; + let view = self.swapchain_image_views[image_index as usize]; + + // 4. Transition the swapchain image to a layout suitable for drawing + let range = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }; + + let barrier_to_draw = vk::ImageMemoryBarrier2::default() + .image(image) + .subresource_range(range) + .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); + + let dependency_info = vk::DependencyInfo::default() + .image_memory_barriers(std::slice::from_ref(&barrier_to_draw)); + + unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency_info) }; + + // 5. Begin Dynamic Rendering with a clear color + let color_attachment = vk::RenderingAttachmentInfo::default() + .image_view(view) + .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .clear_value(vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.1, 0.2, 0.4, 1.0], // Project Catalyst Blue + }, + }); + + let rendering_info = vk::RenderingInfo::default() + .render_area(vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, + }) + .layer_count(1) + .color_attachments(std::slice::from_ref(&color_attachment)); + + unsafe { + self.device.cmd_begin_rendering(cmd, &rendering_info); + // Future draw calls will go here + self.device.cmd_end_rendering(cmd); + } + + // 6. Transition the image back to Present layout + let barrier_to_present = vk::ImageMemoryBarrier2::default() + .image(image) + .subresource_range(range) + .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE) + .dst_access_mask(vk::AccessFlags2::empty()) + .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR); + + let dependency_info = vk::DependencyInfo::default() + .image_memory_barriers(std::slice::from_ref(&barrier_to_present)); + + unsafe { + self.device.cmd_pipeline_barrier2(cmd, &dependency_info); + self.device.end_command_buffer(cmd)?; + } + + // 7. Submit the work to the GPU + let submit_info = vk::SubmitInfo::default() + .wait_semaphores(std::slice::from_ref(&image_available_semaphore)) + .wait_dst_stage_mask(&[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]) + .command_buffers(std::slice::from_ref(&cmd)) + .signal_semaphores(std::slice::from_ref(&render_finished_semaphore)); + + unsafe { + self.device.queue_submit(self.graphics_queue, &[submit_info], in_flight_fence)?; + } + + // 8. Present the result to the screen + let present_info = vk::PresentInfoKHR::default() + .wait_semaphores(std::slice::from_ref(&render_finished_semaphore)) + .swapchains(std::slice::from_ref(&self.swapchain)) + .image_indices(std::slice::from_ref(&image_index)); + + unsafe { + self.swapchain_loader.queue_present(self.graphics_queue, &present_info)?; + } + + // Advance the frame index for the next call + self.current_frame = (self.current_frame + 1) % self.in_flight_fences.len(); + + Ok(()) + } } /// Ensures all Vulkan resources are destroyed in the correct order. impl Drop for Renderer { fn drop(&mut self) { unsafe { + // Ensure the GPU is finished before we start destroying things + let _ = self.device.device_wait_idle(); + self.device.destroy_command_pool(self.command_pool, None); - // Destroy synchronization primitives - self.device.destroy_semaphore(self.image_available_semaphore, None); - self.device.destroy_semaphore(self.render_finished_semaphore, None); - self.device.destroy_fence(self.in_flight_fence, None); + // Destroy synchronization primitives for all frames + for &semaphore in &self.image_available_semaphores { + self.device.destroy_semaphore(semaphore, None); + } + for &semaphore in &self.render_finished_semaphores { + self.device.destroy_semaphore(semaphore, None); + } + for &fence in &self.in_flight_fences { + self.device.destroy_fence(fence, None); + } // Destroy the swapchain self.swapchain_loader.destroy_swapchain(self.swapchain, None);