feat(renderer): render keyed collection of chunk meshes with per-chunk offset
This commit is contained in:
parent
2ca9ae801d
commit
dd8a969828
|
|
@ -26,9 +26,9 @@ use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
|||
use std::ffi::c_char;
|
||||
|
||||
pub use error::RendererError;
|
||||
pub use renderer::Renderer;
|
||||
pub use renderer::{MeshKey, Renderer};
|
||||
|
||||
use crate::mesh::Vertex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl Renderer {
|
||||
/// Initializes the Vulkan renderer.
|
||||
|
|
@ -42,7 +42,7 @@ impl Renderer {
|
|||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `MAX_FRAMES_IN_FLIGHT` or vertex data sizes exceed `u32`/`u64` limits.
|
||||
/// Panics if `MAX_FRAMES_IN_FLIGHT` exceeds `u32`'s range.
|
||||
// TODO: partial-construction leak. Each `?` below early-returns and leaks every Vulkan resource created so far; only a fully successful `new` reaches `Drop for Renderer`. Once the renderer grows more state, wrap each resource in an RAII guard so failure paths tear them down too.
|
||||
pub fn new(
|
||||
display_handle: RawDisplayHandle,
|
||||
|
|
@ -125,11 +125,6 @@ impl Renderer {
|
|||
let graphics_pipeline =
|
||||
pipeline::create_graphics_pipeline(&device, pipeline_layout, swapchain_format)?;
|
||||
|
||||
let (vertex_buffer, vertex_allocation, index_buffer, index_allocation) =
|
||||
create_geometry(&device, &mut allocator)?;
|
||||
|
||||
let index_count = 36;
|
||||
|
||||
let (depth_image, depth_allocation, depth_image_view) =
|
||||
create_depth_resources(&device, &mut allocator, swapchain_extent)?;
|
||||
|
||||
|
|
@ -153,16 +148,12 @@ impl Renderer {
|
|||
command_pool,
|
||||
command_buffers,
|
||||
allocator: Some(allocator),
|
||||
index_buffer,
|
||||
index_allocation: Some(index_allocation),
|
||||
index_count,
|
||||
chunk_meshes: HashMap::new(),
|
||||
depth_image,
|
||||
depth_allocation: Some(depth_allocation),
|
||||
depth_image_view,
|
||||
pipeline_layout,
|
||||
graphics_pipeline,
|
||||
vertex_buffer,
|
||||
vertex_allocation: Some(vertex_allocation),
|
||||
sync: Some(sync),
|
||||
current_frame: 0,
|
||||
})
|
||||
|
|
@ -193,84 +184,6 @@ fn create_allocator(
|
|||
Ok(allocator)
|
||||
}
|
||||
|
||||
/// Creates the 3D geometry buffers (vertex and index) for a cube.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if a buffer cannot be created.
|
||||
fn create_geometry(
|
||||
device: &ash::Device,
|
||||
allocator: &mut Allocator,
|
||||
) -> Result<(vk::Buffer, Allocation, vk::Buffer, Allocation), RendererError> {
|
||||
let vertices = [
|
||||
// Front face
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.5],
|
||||
color: [1.0, 0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.5],
|
||||
color: [0.0, 1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.5],
|
||||
color: [0.0, 0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.5],
|
||||
color: [1.0, 1.0, 1.0],
|
||||
},
|
||||
// Back face
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, -0.5],
|
||||
color: [1.0, 0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, -0.5],
|
||||
color: [0.0, 1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, -0.5],
|
||||
color: [0.0, 0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, -0.5],
|
||||
color: [1.0, 1.0, 1.0],
|
||||
},
|
||||
];
|
||||
let indices: [u32; 36] = [
|
||||
0, 1, 2, 2, 3, 0, // front
|
||||
1, 5, 6, 6, 2, 1, // right
|
||||
7, 6, 5, 5, 4, 7, // back
|
||||
4, 0, 3, 3, 7, 4, // left
|
||||
4, 5, 1, 1, 0, 4, // bottom
|
||||
3, 2, 6, 6, 7, 3, // top
|
||||
];
|
||||
|
||||
let (vertex_buffer, vertex_allocation) = create_gpu_buffer(
|
||||
device,
|
||||
allocator,
|
||||
bytemuck::cast_slice(&vertices),
|
||||
vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||
"Vertex Buffer",
|
||||
)?;
|
||||
|
||||
let (index_buffer, index_allocation) = create_gpu_buffer(
|
||||
device,
|
||||
allocator,
|
||||
bytemuck::cast_slice(&indices),
|
||||
vk::BufferUsageFlags::INDEX_BUFFER,
|
||||
"Index Buffer",
|
||||
)?;
|
||||
|
||||
Ok((
|
||||
vertex_buffer,
|
||||
vertex_allocation,
|
||||
index_buffer,
|
||||
index_allocation,
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates the depth buffer resources (image, memory, and view).
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
|
|
@ -36,16 +36,17 @@ pub fn create_shader_module(
|
|||
///
|
||||
/// Returns [`RendererError::VulkanError`] if the device fails to create the pipeline layout.
|
||||
pub fn create_pipeline_layout(device: &Device) -> Result<vk::PipelineLayout, RendererError> {
|
||||
// A single push constant range is defined for the MVP matrix, allowing it to be updated for every draw call with high efficiency.
|
||||
// The push-constant range covers the 64-byte MVP matrix followed by a 16-byte vec4 per-chunk world offset (80 bytes total, within the 128-byte guaranteed minimum).
|
||||
#[expect(
|
||||
clippy::expect_used,
|
||||
reason = "size_of::<Mat4>() is 64 bytes, well within u32 range"
|
||||
reason = "80 bytes (Mat4 + vec4) is well within u32 range"
|
||||
)]
|
||||
let push_constant_range = vk::PushConstantRange::default()
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX)
|
||||
.offset(0)
|
||||
.size(
|
||||
u32::try_from(std::mem::size_of::<glam::Mat4>()).expect("Mat4 size exceeds u32 range"),
|
||||
u32::try_from(std::mem::size_of::<glam::Mat4>() + std::mem::size_of::<[f32; 4]>())
|
||||
.expect("push-constant size exceeds u32 range"),
|
||||
);
|
||||
|
||||
let layout_create_info = vk::PipelineLayoutCreateInfo::default()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,26 @@ use crate::sync::SyncPrimitives;
|
|||
use crate::{error::RendererError, mesh::Vertex};
|
||||
use ash::{Device, Instance, khr, vk};
|
||||
use gpu_allocator::vulkan::{Allocation, Allocator};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Opaque, renderer-side identifier for one uploaded chunk mesh.
|
||||
pub type MeshKey = (i32, i32, i32);
|
||||
|
||||
/// GPU resources for a single chunk mesh, drawn at a fixed world offset.
|
||||
pub(crate) struct GpuMesh {
|
||||
/// Buffer holding the chunk's vertex data.
|
||||
pub(crate) vertex_buffer: vk::Buffer,
|
||||
/// Backing allocation for [`GpuMesh::vertex_buffer`], freed when the mesh is removed.
|
||||
pub(crate) vertex_allocation: Allocation,
|
||||
/// Buffer holding the chunk's index data for indexed drawing.
|
||||
pub(crate) index_buffer: vk::Buffer,
|
||||
/// Backing allocation for [`GpuMesh::index_buffer`], freed when the mesh is removed.
|
||||
pub(crate) index_allocation: Allocation,
|
||||
/// Number of indices submitted in the mesh's `cmd_draw_indexed` call.
|
||||
pub(crate) index_count: u32,
|
||||
/// Chunk origin in world space (blocks); added to every vertex in the vertex shader.
|
||||
pub(crate) world_offset: [f32; 3],
|
||||
}
|
||||
|
||||
/// The core renderer structure holding the Vulkan resources.
|
||||
pub struct Renderer {
|
||||
|
|
@ -56,15 +76,8 @@ pub struct Renderer {
|
|||
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,
|
||||
/// Uploaded chunk meshes, keyed by an opaque renderer-side handle and drawn independently.
|
||||
pub(crate) chunk_meshes: HashMap<MeshKey, GpuMesh>,
|
||||
/// The depth image used for depth testing.
|
||||
pub(crate) depth_image: vk::Image,
|
||||
/// Image view for the depth buffer.
|
||||
|
|
@ -293,11 +306,6 @@ impl Renderer {
|
|||
};
|
||||
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);
|
||||
|
||||
let aspect =
|
||||
f64::from(self.swapchain_extent.width) / f64::from(self.swapchain_extent.height);
|
||||
|
||||
|
|
@ -315,6 +323,7 @@ impl Renderer {
|
|||
// The view matrix is supplied by the caller (the client's camera); the renderer owns only the projection, which depends on the swapchain aspect ratio it manages.
|
||||
let mvp = projection * camera_view;
|
||||
|
||||
// The MVP is identical for every chunk this frame, so it is pushed once before the loop.
|
||||
let mvp_bytes = bytemuck::cast_slice(mvp.as_ref());
|
||||
self.device.cmd_push_constants(
|
||||
cmd,
|
||||
|
|
@ -324,8 +333,36 @@ impl Renderer {
|
|||
mvp_bytes,
|
||||
);
|
||||
|
||||
// The per-chunk offset occupies the push-constant range immediately after the 64-byte MVP.
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "size_of::<Mat4>() is 64 bytes, well within u32 range"
|
||||
)]
|
||||
let chunk_offset_byte = size_of::<glam::Mat4>() as u32;
|
||||
|
||||
for mesh in self.chunk_meshes.values() {
|
||||
// The offset is padded to a vec4 to match the std140 layout of the push-constant block; only xyz is read by the shader.
|
||||
let offset = [
|
||||
mesh.world_offset[0],
|
||||
mesh.world_offset[1],
|
||||
mesh.world_offset[2],
|
||||
0.0_f32,
|
||||
];
|
||||
self.device.cmd_push_constants(
|
||||
cmd,
|
||||
self.pipeline_layout,
|
||||
vk::ShaderStageFlags::VERTEX,
|
||||
chunk_offset_byte,
|
||||
bytemuck::cast_slice(&offset),
|
||||
);
|
||||
|
||||
self.device
|
||||
.cmd_draw_indexed(cmd, self.index_count, 1, 0, 0, 0);
|
||||
.cmd_bind_vertex_buffers(cmd, 0, &[mesh.vertex_buffer], &[0]);
|
||||
self.device
|
||||
.cmd_bind_index_buffer(cmd, mesh.index_buffer, 0, vk::IndexType::UINT32);
|
||||
self.device
|
||||
.cmd_draw_indexed(cmd, mesh.index_count, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,47 +405,36 @@ impl Renderer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces the currently rendering mesh with a new set of vertices and indices.
|
||||
/// Uploads (or replaces) the mesh stored under `key`, positioned at `world_offset` (in blocks).
|
||||
///
|
||||
/// If a mesh already exists under `key`, its GPU resources are freed before the replacement is
|
||||
/// uploaded.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the vertex or index buffers cannot be created.
|
||||
/// Returns [`RendererError::AllocatorMissing`] if the GPU allocator has been torn down,
|
||||
/// [`RendererError::AllocationError`] if GPU memory cannot be allocated, or
|
||||
/// [`RendererError::VulkanError`] if the vertex or index buffers cannot be created.
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "a chunk mesh's index count never approaches u32::MAX"
|
||||
)]
|
||||
pub fn update_mesh(
|
||||
pub fn insert_mesh(
|
||||
&mut self,
|
||||
key: MeshKey,
|
||||
vertices: &[Vertex],
|
||||
indices: &[u32],
|
||||
world_offset: [f32; 3],
|
||||
) -> Result<(), RendererError> {
|
||||
unsafe {
|
||||
let _ = self.device.device_wait_idle();
|
||||
// Free any mesh already stored under this key before uploading its replacement.
|
||||
self.remove_mesh(key);
|
||||
|
||||
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(
|
||||
let (vertex_buffer, vertex_allocation) = create_gpu_buffer(
|
||||
&self.device,
|
||||
allocator,
|
||||
bytemuck::cast_slice(vertices),
|
||||
|
|
@ -416,7 +442,7 @@ impl Renderer {
|
|||
"Chunk Vertex Buffer",
|
||||
)?;
|
||||
|
||||
let (i_buf, i_alloc) = crate::create_gpu_buffer(
|
||||
let (index_buffer, index_allocation) = create_gpu_buffer(
|
||||
&self.device,
|
||||
allocator,
|
||||
bytemuck::cast_slice(indices),
|
||||
|
|
@ -424,14 +450,39 @@ impl Renderer {
|
|||
"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;
|
||||
self.chunk_meshes.insert(
|
||||
key,
|
||||
GpuMesh {
|
||||
vertex_buffer,
|
||||
vertex_allocation,
|
||||
index_buffer,
|
||||
index_allocation,
|
||||
index_count: indices.len() as u32,
|
||||
world_offset,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Frees the GPU mesh stored under `key`. Does nothing if no mesh is present.
|
||||
pub fn remove_mesh(&mut self, key: MeshKey) {
|
||||
let Some(mesh) = self.chunk_meshes.remove(&key) else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
// Waiting idle per removal is the simple, always-correct approach; in a bulk load/unload loop it serialises the GPU, so a single wait around the loop is preferable if this ever shows up as a measured bottleneck.
|
||||
let _ = self.device.device_wait_idle();
|
||||
|
||||
if let Some(allocator) = self.allocator.as_mut() {
|
||||
let _ = allocator.free(mesh.vertex_allocation);
|
||||
let _ = allocator.free(mesh.index_allocation);
|
||||
}
|
||||
self.device.destroy_buffer(mesh.vertex_buffer, None);
|
||||
self.device.destroy_buffer(mesh.index_buffer, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Renderer {
|
||||
|
|
@ -443,16 +494,18 @@ impl Drop for Renderer {
|
|||
self.device
|
||||
.destroy_pipeline_layout(self.pipeline_layout, None);
|
||||
|
||||
// Drain the chunk meshes so each owned allocation can be freed and its buffers destroyed.
|
||||
let meshes: Vec<GpuMesh> = self.chunk_meshes.drain().map(|(_, mesh)| mesh).collect();
|
||||
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}");
|
||||
for mesh in meshes {
|
||||
if let Err(e) = allocator.free(mesh.vertex_allocation) {
|
||||
tracing::error!("Failed to free chunk vertex 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 Err(e) = allocator.free(mesh.index_allocation) {
|
||||
tracing::error!("Failed to free chunk index allocation: {e}");
|
||||
}
|
||||
self.device.destroy_buffer(mesh.vertex_buffer, None);
|
||||
self.device.destroy_buffer(mesh.index_buffer, None);
|
||||
}
|
||||
if let Some(alloc) = self.depth_allocation.take()
|
||||
&& let Err(e) = allocator.free(alloc)
|
||||
|
|
@ -460,8 +513,6 @@ impl Drop for Renderer {
|
|||
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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue