Synvael/crates/renderer/src/renderer.rs
Serkyo f04912df70 fix(renderer): plug shader-module and stale-handle leaks
create_graphics_pipeline now destroys its shader modules on the error
path as well, and update_mesh nulls the buffer handles after destroying
them so a later Drop or retry cannot redestroy freed buffers.
2026-05-16 19:43:16 +02:00

477 lines
18 KiB
Rust

use crate::create_gpu_buffer;
use crate::sync::SyncPrimitives;
use crate::{error::RendererError, mesh::Vertex};
use ash::{Device, Instance, khr, vk};
use gpu_allocator::vulkan::{Allocation, Allocator};
/// The core renderer structure holding the Vulkan resources.
pub struct Renderer {
/// Entry point to the Vulkan library.
pub(crate) _entry: ash::Entry,
/// The Vulkan instance.
pub(crate) instance: Instance,
/// Optional debug utility loader for validation layers.
pub(crate) debug_utils: Option<ash::ext::debug_utils::Instance>,
/// The debug messenger for validation layer output.
pub(crate) debug_messenger: vk::DebugUtilsMessengerEXT,
/// Handle to the selected physical device (GPU).
#[allow(dead_code)]
pub(crate) physical_device: vk::PhysicalDevice,
/// The logical Vulkan device.
pub(crate) device: Device,
/// The queue used for graphics operations.
pub(crate) graphics_queue: vk::Queue,
/// Index of the graphics queue family.
#[allow(dead_code)]
pub(crate) graphics_queue_index: u32,
/// Surface extension loader.
pub(crate) surface_loader: khr::surface::Instance,
/// The presentation surface.
pub(crate) surface: vk::SurfaceKHR,
/// Swapchain extension loader.
pub(crate) swapchain_loader: khr::swapchain::Device,
/// The swapchain for presenting images.
pub(crate) swapchain: vk::SwapchainKHR,
/// Images acquired from the swapchain.
pub(crate) swapchain_images: Vec<vk::Image>,
/// The pixel format of the swapchain images.
#[allow(dead_code)]
pub(crate) swapchain_format: vk::Format,
/// The dimensions of the swapchain images.
pub(crate) swapchain_extent: vk::Extent2D,
/// Image views for each swapchain image.
pub(crate) swapchain_image_views: Vec<vk::ImageView>,
/// The command pool used for allocating command buffers.
pub(crate) command_pool: vk::CommandPool,
/// Pre-allocated command buffers for each frame in flight.
pub(crate) command_buffers: Vec<vk::CommandBuffer>,
/// The layout of the graphics pipeline.
pub(crate) pipeline_layout: vk::PipelineLayout,
/// The compiled graphics pipeline state.
pub(crate) graphics_pipeline: vk::Pipeline,
/// Memory manager for GPU allocations.
pub(crate) allocator: Option<Allocator>,
/// Buffer containing the vertex data for the initial triangle.
pub(crate) vertex_buffer: vk::Buffer,
/// Memory allocation for the vertex buffer.
pub(crate) vertex_allocation: Option<Allocation>,
/// Buffer containing the index data for indexed drawing.
pub(crate) index_buffer: vk::Buffer,
/// Memory allocation for the index buffer.
pub(crate) index_allocation: Option<Allocation>,
pub(crate) index_count: u32,
/// The depth image used for depth testing.
pub(crate) depth_image: vk::Image,
/// Image view for the depth buffer.
pub(crate) depth_image_view: vk::ImageView,
/// Memory allocation for the depth image.
pub(crate) depth_allocation: Option<Allocation>,
/// Synchronization primitives for frame-by-frame execution.
pub(crate) sync: Option<SyncPrimitives>,
/// Index of the current frame being processed (0 to `crate::MAX_FRAMES_IN_FLIGHT` - 1).
pub(crate) current_frame: usize,
}
impl Renderer {
/// Renders a single frame.
pub fn draw_frame(&mut self) -> Result<(), RendererError> {
let sync = self
.sync
.as_ref()
.ok_or(RendererError::SyncPrimitivesMissing)?;
let in_flight_fence = sync.in_flight[self.current_frame];
let image_available_semaphore = sync.image_available[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(),
)?
};
// Use the semaphore tied to this specific swapchain image for rendering completion
let render_finished_semaphore = self
.sync
.as_ref()
.ok_or(RendererError::SyncPrimitivesMissing)?
.render_finished[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. Record the actual rendering commands
self.record_commands(cmd, view, image)?;
// 5. 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)?;
}
// 6. 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) % crate::MAX_FRAMES_IN_FLIGHT;
Ok(())
}
/// Records the drawing commands into the given command buffer.
fn record_commands(
&self,
cmd: vk::CommandBuffer,
view: vk::ImageView,
image: vk::Image,
) -> Result<(), RendererError> {
// Transition layouts for drawing
self.transition_to_draw_layout(cmd, image);
// Begin rendering
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],
},
});
let depth_attachment = vk::RenderingAttachmentInfo::default()
.image_view(self.depth_image_view)
.image_layout(vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.clear_value(vk::ClearValue {
depth_stencil: vk::ClearDepthStencilValue {
depth: 1.0,
stencil: 0,
},
});
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))
.depth_attachment(&depth_attachment);
unsafe {
self.device.cmd_begin_rendering(cmd, &rendering_info);
self.issue_draw_calls(cmd);
self.device.cmd_end_rendering(cmd);
}
// Transition back to present
self.transition_to_present_layout(cmd, image)?;
Ok(())
}
/// Transitions the swapchain and depth images to layouts suitable for drawing.
fn transition_to_draw_layout(&self, cmd: vk::CommandBuffer, image: vk::Image) {
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 depth_range = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::DEPTH,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
let depth_barrier = vk::ImageMemoryBarrier2::default()
.image(self.depth_image)
.subresource_range(depth_range)
.src_stage_mask(vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS)
.src_access_mask(vk::AccessFlags2::empty())
.dst_stage_mask(vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS)
.dst_access_mask(vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL);
let barriers = [barrier_to_draw, depth_barrier];
let dependency_info = vk::DependencyInfo::default().image_memory_barriers(&barriers);
unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency_info) };
}
/// Issues the actual draw calls for the frame.
fn issue_draw_calls(&self, cmd: vk::CommandBuffer) {
unsafe {
self.device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::GRAPHICS,
self.graphics_pipeline,
);
#[allow(clippy::cast_precision_loss)]
let viewport = vk::Viewport {
x: 0.0,
y: 0.0,
width: self.swapchain_extent.width as f32,
height: self.swapchain_extent.height as f32,
min_depth: 0.0,
max_depth: 1.0,
};
self.device.cmd_set_viewport(cmd, 0, &[viewport]);
let scissor = vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: self.swapchain_extent,
};
self.device.cmd_set_scissor(cmd, 0, &[scissor]);
self.device
.cmd_bind_vertex_buffers(cmd, 0, &[self.vertex_buffer], &[0]);
self.device
.cmd_bind_index_buffer(cmd, self.index_buffer, 0, vk::IndexType::UINT32);
#[allow(clippy::cast_precision_loss)]
let aspect =
f64::from(self.swapchain_extent.width) / f64::from(self.swapchain_extent.height);
#[allow(clippy::cast_possible_truncation)]
let mut projection =
glam::Mat4::perspective_rh(45.0_f32.to_radians(), aspect as f32, 0.1, 500.0);
projection.col_mut(1).y *= -1.0;
let view = glam::Mat4::look_at_rh(
glam::vec3(16.0, 40.0, 60.0),
glam::vec3(16.0, 16.0, 16.0),
glam::vec3(0.0, 1.0, 0.0),
);
let model = glam::Mat4::from_rotation_y(0.0);
let mvp = projection * view * model;
let mvp_bytes = bytemuck::cast_slice(mvp.as_ref());
self.device.cmd_push_constants(
cmd,
self.pipeline_layout,
vk::ShaderStageFlags::VERTEX,
0,
mvp_bytes,
);
self.device
.cmd_draw_indexed(cmd, self.index_count, 1, 0, 0, 0);
}
}
/// Transitions the swapchain image back to the presentation layout.
fn transition_to_present_layout(
&self,
cmd: vk::CommandBuffer,
image: vk::Image,
) -> Result<(), RendererError> {
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_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)?;
}
Ok(())
}
/// Replaces the currently rendering mesh with a new set of vertices and indices.
///
/// # Errors
/// Returns a `RendererError` if new Vulkan buffers cannot be allocated or created.
#[allow(clippy::cast_possible_truncation)]
pub fn update_mesh(
&mut self,
vertices: &[Vertex],
indices: &[u32],
) -> Result<(), RendererError> {
unsafe {
let _ = self.device.device_wait_idle();
let allocator = self
.allocator
.as_mut()
.ok_or(RendererError::AllocatorMissing)?;
if let Some(alloc) = self.vertex_allocation.take() {
let _ = allocator.free(alloc);
}
self.device.destroy_buffer(self.vertex_buffer, None);
self.vertex_buffer = vk::Buffer::null();
if let Some(alloc) = self.index_allocation.take() {
let _ = allocator.free(alloc);
}
self.device.destroy_buffer(self.index_buffer, None);
self.index_buffer = vk::Buffer::null();
}
let allocator = self
.allocator
.as_mut()
.ok_or(RendererError::AllocatorMissing)?;
let (v_buf, v_alloc) = create_gpu_buffer(
&self.device,
allocator,
bytemuck::cast_slice(vertices),
vk::BufferUsageFlags::VERTEX_BUFFER,
"Chunk Vertex Buffer",
)?;
let (i_buf, i_alloc) = crate::create_gpu_buffer(
&self.device,
allocator,
bytemuck::cast_slice(indices),
vk::BufferUsageFlags::INDEX_BUFFER,
"Chunk Index Buffer",
)?;
self.vertex_buffer = v_buf;
self.vertex_allocation = Some(v_alloc);
self.index_buffer = i_buf;
self.index_allocation = Some(i_alloc);
self.index_count = indices.len() as u32;
Ok(())
}
}
impl Drop for Renderer {
fn drop(&mut self) {
unsafe {
let _ = self.device.device_wait_idle();
self.device.destroy_pipeline(self.graphics_pipeline, None);
self.device
.destroy_pipeline_layout(self.pipeline_layout, None);
if let Some(allocator) = self.allocator.as_mut() {
if let Some(alloc) = self.vertex_allocation.take()
&& let Err(e) = allocator.free(alloc)
{
tracing::error!("Failed to free vertex buffer allocation: {e}");
}
if let Some(alloc) = self.index_allocation.take()
&& let Err(e) = allocator.free(alloc)
{
tracing::error!("Failed to free index buffer allocation: {e}");
}
if let Some(alloc) = self.depth_allocation.take()
&& let Err(e) = allocator.free(alloc)
{
tracing::error!("Failed to free depth image allocation: {e}");
}
}
self.device.destroy_buffer(self.vertex_buffer, None);
self.device.destroy_buffer(self.index_buffer, None);
self.device.destroy_image_view(self.depth_image_view, None);
self.device.destroy_image(self.depth_image, None);
// Drop the allocator before destroying the logical device so it can
// release any remaining memory blocks while the device is still valid.
drop(self.allocator.take());
self.device.destroy_command_pool(self.command_pool, None);
if let Some(sync) = self.sync.take() {
crate::sync::destroy_sync_primitives(&self.device, sync);
}
// Destroy image views before the swapchain that owns the underlying images.
for &view in &self.swapchain_image_views {
self.device.destroy_image_view(view, None);
}
self.swapchain_loader
.destroy_swapchain(self.swapchain, None);
// Destroy the logical device
self.device.destroy_device(None);
// Destroy the surface
self.surface_loader.destroy_surface(self.surface, None);
// Destroy the debug messenger if it exists
if let Some(debug_utils) = self.debug_utils.as_ref() {
debug_utils.destroy_debug_utils_messenger(self.debug_messenger, None);
}
// Destroy the instance
self.instance.destroy_instance(None);
}
}
}