refactor(client): abstract chunk mesh upload behind a MeshSink trait
This commit is contained in:
parent
2abe08bf57
commit
dae0bafcd3
|
|
@ -6,6 +6,8 @@ use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use renderer::meshing::Neighbors;
|
use renderer::meshing::Neighbors;
|
||||||
|
use renderer::vertex::Vertex;
|
||||||
|
use renderer::{MeshKey, RendererError};
|
||||||
use shared::protocol::chunk::ChunkMessage;
|
use shared::protocol::chunk::ChunkMessage;
|
||||||
use shared::world::{CHUNK_SIZE, Chunk, ChunkPos};
|
use shared::world::{CHUNK_SIZE, Chunk, ChunkPos};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
@ -31,6 +33,43 @@ const NEIGHBOR_OFFSETS: [(i32, i32, i32); 6] = [
|
||||||
(0, 0, -1),
|
(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.
|
/// Tracks which server-streamed chunks are resident and orchestrates neighbour-aware meshing.
|
||||||
pub struct ChunkManager {
|
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.
|
/// 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,
|
&mut self,
|
||||||
center: ChunkPos,
|
center: ChunkPos,
|
||||||
deliveries: &mut net::ChunkStream,
|
deliveries: &mut net::ChunkStream,
|
||||||
renderer: &mut renderer::Renderer,
|
sink: &mut impl MeshSink,
|
||||||
) {
|
) {
|
||||||
let unloaded = self.unload_outside(center, renderer);
|
let unloaded = self.unload_outside(center, sink);
|
||||||
let (loaded, dropped) = self.apply_deliveries(deliveries, renderer);
|
let (loaded, dropped) = self.apply_deliveries(deliveries, sink);
|
||||||
let meshed = self.drain_remesh(renderer);
|
let meshed = self.drain_remesh(sink);
|
||||||
|
|
||||||
if loaded > 0 || dropped > 0 || unloaded > 0 || meshed > 0 {
|
if loaded > 0 || dropped > 0 || unloaded > 0 || meshed > 0 {
|
||||||
debug!(
|
debug!(
|
||||||
|
|
@ -85,7 +124,7 @@ impl ChunkManager {
|
||||||
fn apply_deliveries(
|
fn apply_deliveries(
|
||||||
&mut self,
|
&mut self,
|
||||||
deliveries: &mut net::ChunkStream,
|
deliveries: &mut net::ChunkStream,
|
||||||
renderer: &mut renderer::Renderer,
|
sink: &mut impl MeshSink,
|
||||||
) -> (usize, usize) {
|
) -> (usize, usize) {
|
||||||
let mut loaded = Vec::new();
|
let mut loaded = Vec::new();
|
||||||
let mut dropped = Vec::new();
|
let mut dropped = Vec::new();
|
||||||
|
|
@ -99,7 +138,7 @@ impl ChunkManager {
|
||||||
}
|
}
|
||||||
Ok(ChunkMessage::Drop { pos }) => {
|
Ok(ChunkMessage::Drop { pos }) => {
|
||||||
if self.resident.remove(&pos).is_some() {
|
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);
|
dropped.push(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +154,7 @@ impl ChunkManager {
|
||||||
/// Evicts every resident chunk outside the load radius around `center`, returning the number removed.
|
/// 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.
|
/// 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 desired = desired_chunks(center, LOAD_RADIUS);
|
||||||
let stale: Vec<ChunkPos> = self
|
let stale: Vec<ChunkPos> = self
|
||||||
.resident
|
.resident
|
||||||
|
|
@ -124,7 +163,7 @@ impl ChunkManager {
|
||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
for pos in &stale {
|
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.resident.remove(pos);
|
||||||
}
|
}
|
||||||
self.queue_remesh(&[], &stale);
|
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.
|
/// 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.
|
/// 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.
|
// Take a bounded batch out of the set; the remainder stays queued for later frames.
|
||||||
let batch: Vec<ChunkPos> = self
|
let batch: Vec<ChunkPos> = self
|
||||||
.pending_remesh
|
.pending_remesh
|
||||||
|
|
@ -153,17 +192,17 @@ impl ChunkManager {
|
||||||
for pos in batch {
|
for pos in batch {
|
||||||
self.pending_remesh.remove(&pos);
|
self.pending_remesh.remove(&pos);
|
||||||
if self.resident.contains_key(&pos) {
|
if self.resident.contains_key(&pos) {
|
||||||
self.mesh_and_upload(pos, renderer);
|
self.mesh_and_upload(pos, sink);
|
||||||
meshed += 1;
|
meshed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meshed
|
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.
|
/// 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, renderer: &mut renderer::Renderer) {
|
fn mesh_and_upload(&mut self, pos: ChunkPos, sink: &mut impl MeshSink) {
|
||||||
let Some(chunk) = self.resident.get(&pos) else {
|
let Some(chunk) = self.resident.get(&pos) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -171,7 +210,7 @@ impl ChunkManager {
|
||||||
let (vertices, indices) = renderer::meshing::generate_mesh(chunk, &neighbors);
|
let (vertices, indices) = renderer::meshing::generate_mesh(chunk, &neighbors);
|
||||||
|
|
||||||
if indices.is_empty() {
|
if indices.is_empty() {
|
||||||
renderer.remove_mesh((pos.x, pos.y, pos.z));
|
sink.remove_mesh((pos.x, pos.y, pos.z));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,9 +228,7 @@ impl ChunkManager {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) = sink.insert_mesh((pos.x, pos.y, pos.z), &vertices, &indices, world_offset) {
|
||||||
renderer.insert_mesh((pos.x, pos.y, pos.z), &vertices, &indices, world_offset)
|
|
||||||
{
|
|
||||||
error!(?pos, "failed to upload chunk mesh: {e}");
|
error!(?pos, "failed to upload chunk mesh: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue