// 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, /// 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>; 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, /// Generated triangle indices; empty when the chunk meshes to no geometry. pub(crate) indices: Vec, /// 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, /// Receiving end of the result queue; the main thread drains finished meshes. result_rx: Receiver, /// 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>, } 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::(); let (result_tx, result_rx) = crossbeam_channel::unbounded::(); // 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 { 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, Vec) { 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) }