refactor(client): abstract chunk mesh upload behind a MeshSink trait

This commit is contained in:
Serkyo 2026-07-26 20:19:27 +02:00
parent 2abe08bf57
commit dae0bafcd3

View file

@ -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<ChunkPos> = 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<ChunkPos> = 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}");
}
}