From dae0bafcd323d3abdf4711083e163a1744b6c9c8 Mon Sep 17 00:00:00 2001 From: Serkyo Date: Sun, 26 Jul 2026 20:19:27 +0200 Subject: [PATCH] refactor(client): abstract chunk mesh upload behind a MeshSink trait --- crates/client/src/chunks.rs | 71 ++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/crates/client/src/chunks.rs b/crates/client/src/chunks.rs index ca22e7a..f484112 100644 --- a/crates/client/src/chunks.rs +++ b/crates/client/src/chunks.rs @@ -6,6 +6,8 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use renderer::meshing::Neighbors; +use renderer::vertex::Vertex; +use renderer::{MeshKey, RendererError}; use shared::protocol::chunk::ChunkMessage; use shared::world::{CHUNK_SIZE, Chunk, ChunkPos}; use tracing::{debug, error}; @@ -31,6 +33,43 @@ const NEIGHBOR_OFFSETS: [(i32, i32, i32); 6] = [ (0, 0, -1), ]; +/// Sink that receives finished chunk meshes for upload. +/// +/// The production sink is the Vulkan [`Renderer`](renderer::Renderer); the abstraction exists so the meshing pipeline can be exercised against a recording double in tests, which have no GPU. Method signatures mirror the renderer's exactly so the production `impl` is a direct forward. +pub trait MeshSink { + /// Uploads (or replaces) the mesh identified by `key`. + /// + /// # Errors + /// + /// Returns [`RendererError`] when the underlying implementation fails to allocate or write the GPU buffers for the mesh. + fn insert_mesh( + &mut self, + key: MeshKey, + vertices: &[Vertex], + indices: &[u32], + world_offset: [f32; 3], + ) -> Result<(), RendererError>; + + /// Removes any mesh currently associated with `key`; a no-op when none exists. + fn remove_mesh(&mut self, key: MeshKey); +} + +impl MeshSink for renderer::Renderer { + fn insert_mesh( + &mut self, + key: MeshKey, + vertices: &[Vertex], + indices: &[u32], + world_offset: [f32; 3], + ) -> Result<(), RendererError> { + renderer::Renderer::insert_mesh(self, key, vertices, indices, world_offset) + } + + fn remove_mesh(&mut self, key: MeshKey) { + renderer::Renderer::remove_mesh(self, key); + } +} + /// Tracks which server-streamed chunks are resident and orchestrates neighbour-aware meshing. pub struct ChunkManager { /// Resident chunks keyed by position, retained so the mesher can sample voxels across chunk boundaries. Stored behind [`Arc`] so a future worker pool can hand a chunk to a thread without copying it. @@ -60,11 +99,11 @@ impl ChunkManager { &mut self, center: ChunkPos, deliveries: &mut net::ChunkStream, - renderer: &mut renderer::Renderer, + sink: &mut impl MeshSink, ) { - let unloaded = self.unload_outside(center, renderer); - let (loaded, dropped) = self.apply_deliveries(deliveries, renderer); - let meshed = self.drain_remesh(renderer); + let unloaded = self.unload_outside(center, sink); + let (loaded, dropped) = self.apply_deliveries(deliveries, sink); + let meshed = self.drain_remesh(sink); if loaded > 0 || dropped > 0 || unloaded > 0 || meshed > 0 { debug!( @@ -85,7 +124,7 @@ impl ChunkManager { fn apply_deliveries( &mut self, deliveries: &mut net::ChunkStream, - renderer: &mut renderer::Renderer, + sink: &mut impl MeshSink, ) -> (usize, usize) { let mut loaded = Vec::new(); let mut dropped = Vec::new(); @@ -99,7 +138,7 @@ impl ChunkManager { } Ok(ChunkMessage::Drop { pos }) => { if self.resident.remove(&pos).is_some() { - renderer.remove_mesh((pos.x, pos.y, pos.z)); + sink.remove_mesh((pos.x, pos.y, pos.z)); dropped.push(pos); } } @@ -115,7 +154,7 @@ impl ChunkManager { /// Evicts every resident chunk outside the load radius around `center`, returning the number removed. /// /// Each evicted chunk's resident neighbours have a boundary toward it that is now exposed, so they are enqueued for re-meshing. - fn unload_outside(&mut self, center: ChunkPos, renderer: &mut renderer::Renderer) -> usize { + fn unload_outside(&mut self, center: ChunkPos, sink: &mut impl MeshSink) -> usize { let desired = desired_chunks(center, LOAD_RADIUS); let stale: Vec = self .resident @@ -124,7 +163,7 @@ impl ChunkManager { .copied() .collect(); for pos in &stale { - renderer.remove_mesh((pos.x, pos.y, pos.z)); + sink.remove_mesh((pos.x, pos.y, pos.z)); self.resident.remove(pos); } self.queue_remesh(&[], &stale); @@ -140,7 +179,7 @@ impl ChunkManager { /// Meshes and uploads up to [`MESHES_PER_UPDATE`] chunks from the pending set, returning the number processed. /// /// Positions no longer resident (dropped after being enqueued) are discarded without meshing. - fn drain_remesh(&mut self, renderer: &mut renderer::Renderer) -> usize { + fn drain_remesh(&mut self, sink: &mut impl MeshSink) -> usize { // Take a bounded batch out of the set; the remainder stays queued for later frames. let batch: Vec = self .pending_remesh @@ -153,17 +192,17 @@ impl ChunkManager { for pos in batch { self.pending_remesh.remove(&pos); if self.resident.contains_key(&pos) { - self.mesh_and_upload(pos, renderer); + self.mesh_and_upload(pos, sink); meshed += 1; } } meshed } - /// Meshes the resident chunk at `pos` against its resident neighbours and uploads the result to the renderer. + /// Meshes the resident chunk at `pos` against its resident neighbours and uploads the result to the sink. /// - /// A chunk that meshes to no geometry (all air, or fully enclosed by solid neighbours) is removed from the renderer rather than uploaded, since a zero-length buffer is invalid; this also clears any mesh a previous state had left there. - fn mesh_and_upload(&mut self, pos: ChunkPos, renderer: &mut renderer::Renderer) { + /// A chunk that meshes to no geometry (all air, or fully enclosed by solid neighbours) is removed from the sink rather than uploaded, since a zero-length buffer is invalid; this also clears any mesh a previous state had left there. + fn mesh_and_upload(&mut self, pos: ChunkPos, sink: &mut impl MeshSink) { let Some(chunk) = self.resident.get(&pos) else { return; }; @@ -171,7 +210,7 @@ impl ChunkManager { let (vertices, indices) = renderer::meshing::generate_mesh(chunk, &neighbors); if indices.is_empty() { - renderer.remove_mesh((pos.x, pos.y, pos.z)); + sink.remove_mesh((pos.x, pos.y, pos.z)); return; } @@ -189,9 +228,7 @@ impl ChunkManager { ] }; - if let Err(e) = - renderer.insert_mesh((pos.x, pos.y, pos.z), &vertices, &indices, world_offset) - { + if let Err(e) = sink.insert_mesh((pos.x, pos.y, pos.z), &vertices, &indices, world_offset) { error!(?pos, "failed to upload chunk mesh: {e}"); } }