diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index 6bb5f49..d9b0b2b 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -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 diff --git a/crates/renderer/src/pipeline.rs b/crates/renderer/src/pipeline.rs index 0995dfe..4a3e5bc 100644 --- a/crates/renderer/src/pipeline.rs +++ b/crates/renderer/src/pipeline.rs @@ -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 { - // 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::() 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::()).expect("Mat4 size exceeds u32 range"), + u32::try_from(std::mem::size_of::() + std::mem::size_of::<[f32; 4]>()) + .expect("push-constant size exceeds u32 range"), ); let layout_create_info = vk::PipelineLayoutCreateInfo::default() diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index ac5efee..aba68c3 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -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, - /// 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, - /// 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, - pub(crate) index_count: u32, + /// Uploaded chunk meshes, keyed by an opaque renderer-side handle and drawn independently. + pub(crate) chunk_meshes: HashMap, /// 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, ); - self.device - .cmd_draw_indexed(cmd, self.index_count, 1, 0, 0, 0); + // The per-chunk offset occupies the push-constant range immediately after the 64-byte MVP. + #[expect( + clippy::cast_possible_truncation, + reason = "size_of::() is 64 bytes, well within u32 range" + )] + let chunk_offset_byte = size_of::() 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_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(); - - 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(); - } + // 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)?; - 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 = 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}"); - } - if let Some(alloc) = self.index_allocation.take() - && let Err(e) = allocator.free(alloc) - { - tracing::error!("Failed to free index 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 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);