feat(renderer): implement robust synchronized render loop
This commit is contained in:
parent
e005bf1c75
commit
1805ae6aa6
|
|
@ -50,19 +50,10 @@ impl ApplicationHandler for App {
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
},
|
},
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
// Redraw the application.
|
if let Some(renderer) = self.renderer.as_mut() {
|
||||||
//
|
renderer.draw_frame().expect("Failed to draw frame");
|
||||||
// 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.
|
|
||||||
self.window.as_ref().unwrap().request_redraw();
|
self.window.as_ref().unwrap().request_redraw();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,18 @@ pub struct Renderer {
|
||||||
swapchain_image_views: Vec<vk::ImageView>,
|
swapchain_image_views: Vec<vk::ImageView>,
|
||||||
/// The pool used to allocate command buffers.
|
/// The pool used to allocate command buffers.
|
||||||
command_pool: vk::CommandPool,
|
command_pool: vk::CommandPool,
|
||||||
/// The buffer used to record GPU commands.
|
/// The buffers used to record GPU commands (one per frame in flight).
|
||||||
command_buffer: vk::CommandBuffer,
|
command_buffers: Vec<vk::CommandBuffer>,
|
||||||
/// Signaled when the swapchain has provided an image to render into.
|
/// Semaphores signaled when an image is acquired (one per frame in flight).
|
||||||
image_available_semaphore: vk::Semaphore,
|
image_available_semaphores: Vec<vk::Semaphore>,
|
||||||
/// Signaled when rendering is complete and the image is ready for presentation.
|
/// Semaphores signaled when rendering is complete (one per frame in flight).
|
||||||
render_finished_semaphore: vk::Semaphore,
|
render_finished_semaphores: Vec<vk::Semaphore>,
|
||||||
/// Signaled when the GPU has finished executing the command buffer.
|
/// Fences signaled when the GPU has finished a frame (one per frame in flight).
|
||||||
in_flight_fence: vk::Fence,
|
in_flight_fences: Vec<vk::Fence>,
|
||||||
|
/// Tracks which frame is using which swapchain image (one per swapchain image).
|
||||||
|
images_in_flight: Vec<vk::Fence>,
|
||||||
|
/// The index of the frame currently being processed (0..MAX_FRAMES_IN_FLIGHT).
|
||||||
|
current_frame: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
|
|
@ -146,22 +150,41 @@ impl Renderer {
|
||||||
|
|
||||||
let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? };
|
let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? };
|
||||||
|
|
||||||
// Allocate the main command buffer used for rendering
|
// Create synchronization primitives
|
||||||
let alloc_info = vk::CommandBufferAllocateInfo::default()
|
// We use MAX_FRAMES_IN_FLIGHT (2) to allow the CPU to stay one frame ahead of the GPU.
|
||||||
.command_pool(command_pool)
|
const MAX_FRAMES_IN_FLIGHT: usize = 2;
|
||||||
.level(vk::CommandBufferLevel::PRIMARY)
|
let mut image_available_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
|
||||||
.command_buffer_count(1);
|
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 semaphore_info = vk::SemaphoreCreateInfo::default();
|
||||||
let fence_info = vk::FenceCreateInfo::default()
|
let fence_info = vk::FenceCreateInfo::default()
|
||||||
.flags(vk::FenceCreateFlags::SIGNALED);
|
.flags(vk::FenceCreateFlags::SIGNALED);
|
||||||
|
|
||||||
let image_available_semaphore = unsafe { device.create_semaphore(&semaphore_info, None)? };
|
let alloc_info = vk::CommandBufferAllocateInfo::default()
|
||||||
let render_finished_semaphore = unsafe { device.create_semaphore(&semaphore_info, None)? };
|
.command_pool(command_pool)
|
||||||
let in_flight_fence = unsafe { device.create_fence(&fence_info, None)? };
|
.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 {
|
Ok(Self {
|
||||||
_entry: entry,
|
_entry: entry,
|
||||||
|
|
@ -181,10 +204,12 @@ impl Renderer {
|
||||||
swapchain_extent,
|
swapchain_extent,
|
||||||
swapchain_image_views: image_views,
|
swapchain_image_views: image_views,
|
||||||
command_pool,
|
command_pool,
|
||||||
command_buffer,
|
command_buffers,
|
||||||
image_available_semaphore,
|
image_available_semaphores,
|
||||||
render_finished_semaphore,
|
render_finished_semaphores,
|
||||||
in_flight_fence,
|
in_flight_fences,
|
||||||
|
images_in_flight,
|
||||||
|
current_frame: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,18 +414,170 @@ impl Renderer {
|
||||||
|
|
||||||
Ok(views)
|
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.
|
/// Ensures all Vulkan resources are destroyed in the correct order.
|
||||||
impl Drop for Renderer {
|
impl Drop for Renderer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
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);
|
self.device.destroy_command_pool(self.command_pool, None);
|
||||||
|
|
||||||
// Destroy synchronization primitives
|
// Destroy synchronization primitives for all frames
|
||||||
self.device.destroy_semaphore(self.image_available_semaphore, None);
|
for &semaphore in &self.image_available_semaphores {
|
||||||
self.device.destroy_semaphore(self.render_finished_semaphore, None);
|
self.device.destroy_semaphore(semaphore, None);
|
||||||
self.device.destroy_fence(self.in_flight_fence, 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
|
// Destroy the swapchain
|
||||||
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
|
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue