151 lines
6.6 KiB
Rust
151 lines
6.6 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
//! Background worker pool that meshes chunks off the winit thread.
|
|
|
|
use std::num::NonZero;
|
|
use std::sync::Arc;
|
|
use std::thread::JoinHandle;
|
|
|
|
use crossbeam_channel::{Receiver, Sender};
|
|
use renderer::meshing::{Neighbors, generate_mesh};
|
|
use renderer::vertex::Vertex;
|
|
use shared::world::{Chunk, ChunkPos};
|
|
|
|
/// Monotonic staleness token stamped on every dispatched [`MeshJob`].
|
|
///
|
|
/// Between dispatching a job for a position and the worker returning it, that position may have been evicted or re-dispatched with fresher neighbours (a neighbour loaded or dropped). A returned mesh is applied only when its generation still matches the latest generation recorded for the position; older generations are superseded and discarded.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub(crate) struct JobGen(u64);
|
|
|
|
impl JobGen {
|
|
/// The generation of the first job ever dispatched.
|
|
pub(crate) const FIRST: Self = Self(0);
|
|
|
|
/// Returns the next generation after `self`.
|
|
///
|
|
/// Wraps on overflow rather than panicking; wrap-around requires 2^64 dispatches in one session, at which point a collision would additionally require the wrapped-to job to still be outstanding, which is unreachable in practice.
|
|
pub(crate) fn next(self) -> Self {
|
|
Self(self.0.wrapping_add(1))
|
|
}
|
|
}
|
|
|
|
/// A unit of meshing work handed to a worker: an owned snapshot so the worker borrows nothing from the manager.
|
|
///
|
|
/// The chunk and its neighbours are carried as [`Arc`] handles so dispatch is a cheap refcount bump rather than a copy of the 64 KiB voxel volume. Meshing is neighbour-dependent (boundary faces are culled against adjacent chunks), so the six face-adjacent neighbours are snapshotted at dispatch time; a `None` entry means that neighbour is not resident and the boundary is treated as exposed.
|
|
pub(crate) struct MeshJob {
|
|
/// Chunk-space position of the chunk to mesh.
|
|
pub(crate) pos: ChunkPos,
|
|
/// Staleness token identifying this dispatch; echoed back on the result.
|
|
pub(crate) generation: JobGen,
|
|
/// The chunk to mesh.
|
|
pub(crate) chunk: Arc<Chunk>,
|
|
/// The six face-adjacent neighbours, ordered `[+X, -X, +Y, -Y, +Z, -Z]` to match `chunks::NEIGHBOR_OFFSETS`. `None` marks an absent neighbour.
|
|
pub(crate) neighbors: [Option<Arc<Chunk>>; 6],
|
|
}
|
|
|
|
/// A finished mesh returned from a worker to the main thread for upload.
|
|
pub(crate) struct MeshResult {
|
|
/// Chunk-space position the mesh belongs to.
|
|
pub(crate) pos: ChunkPos,
|
|
/// Generated vertices; empty when the chunk meshes to no geometry.
|
|
pub(crate) vertices: Vec<Vertex>,
|
|
/// Generated triangle indices; empty when the chunk meshes to no geometry.
|
|
pub(crate) indices: Vec<u32>,
|
|
/// The generation stamped on the originating [`MeshJob`], used to discard superseded results.
|
|
pub(crate) generation: JobGen,
|
|
}
|
|
|
|
/// A pool of worker threads that mesh chunks and return CPU geometry.
|
|
pub(crate) struct MeshPool {
|
|
/// Sending end of the job queue; the main thread pushes [`MeshJob`]s.
|
|
job_tx: Sender<MeshJob>,
|
|
/// Receiving end of the result queue; the main thread drains finished meshes.
|
|
result_rx: Receiver<MeshResult>,
|
|
/// Handles to the worker threads, retained for a future graceful-stop path that drops `job_tx` and joins them; the process currently relies on OS teardown at exit.
|
|
#[expect(
|
|
dead_code,
|
|
reason = "retained for a future graceful-shutdown join path, mirroring the server pool"
|
|
)]
|
|
workers: Vec<JoinHandle<()>>,
|
|
}
|
|
|
|
impl MeshPool {
|
|
/// Spawns the worker pool, sizing it to leave one logical core for the main thread.
|
|
pub(crate) fn new() -> Self {
|
|
let (job_tx, job_rx) = crossbeam_channel::unbounded::<MeshJob>();
|
|
let (result_tx, result_rx) = crossbeam_channel::unbounded::<MeshResult>();
|
|
|
|
// One worker per logical core, less one to keep the winit thread responsive, but never fewer than one.
|
|
let cores = std::thread::available_parallelism().map_or(4, NonZero::get);
|
|
let worker_count = cores.saturating_sub(1).max(1);
|
|
|
|
let workers = (0..worker_count)
|
|
.map(|_| {
|
|
// Each worker owns its own clone of the shared job queue and of the sender back into the result queue.
|
|
let job_rx = job_rx.clone();
|
|
let result_tx = result_tx.clone();
|
|
|
|
std::thread::spawn(move || {
|
|
// Block until a job arrives; a blocking recv is fine off the main thread.
|
|
while let Ok(job) = job_rx.recv() {
|
|
let (vertices, indices) = mesh_job(&job);
|
|
let result = MeshResult {
|
|
pos: job.pos,
|
|
vertices,
|
|
indices,
|
|
generation: job.generation,
|
|
};
|
|
// A send error means the main thread has gone away; the worker winds down.
|
|
if result_tx.send(result).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
// Drop the template ends left over after cloning so the channels close once the real holders are gone: workers observe job-channel shutdown, and the main thread observes result-channel shutdown.
|
|
drop(job_rx);
|
|
drop(result_tx);
|
|
|
|
Self {
|
|
job_tx,
|
|
result_rx,
|
|
workers,
|
|
}
|
|
}
|
|
|
|
/// Enqueues a meshing job for the pool.
|
|
///
|
|
/// A send error (the workers have shut down) is ignored: there is nothing useful to do with the job, and shutdown only happens at process teardown.
|
|
pub(crate) fn dispatch(&self, job: MeshJob) {
|
|
let _ = self.job_tx.send(job);
|
|
}
|
|
|
|
/// Returns the next finished mesh without blocking, or `None` when none is ready.
|
|
pub(crate) fn poll(&self) -> Option<MeshResult> {
|
|
self.result_rx.try_recv().ok()
|
|
}
|
|
}
|
|
|
|
impl Default for MeshPool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Reconstructs a borrowed [`Neighbors`] view from a job's owned neighbour [`Arc`]s and meshes the chunk.
|
|
///
|
|
/// The [`Neighbors`] view borrows `&Chunk` out of the job's `Arc`s, so it is built and consumed here in one scope while those `Arc`s are still alive.
|
|
fn mesh_job(job: &MeshJob) -> (Vec<Vertex>, Vec<u32>) {
|
|
let neighbors = Neighbors {
|
|
pos_x: job.neighbors[0].as_deref(),
|
|
neg_x: job.neighbors[1].as_deref(),
|
|
pos_y: job.neighbors[2].as_deref(),
|
|
neg_y: job.neighbors[3].as_deref(),
|
|
pos_z: job.neighbors[4].as_deref(),
|
|
neg_z: job.neighbors[5].as_deref(),
|
|
};
|
|
generate_mesh(&job.chunk, &neighbors)
|
|
}
|