feat(server): generate chunks on worker pool
This commit is contained in:
parent
45c3351d7d
commit
9001cc6f98
44
Cargo.lock
generated
44
Cargo.lock
generated
|
|
@ -560,20 +560,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-channel"
|
||||||
version = "0.8.7"
|
version = "0.5.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
|
||||||
dependencies = [
|
|
||||||
"crossbeam-epoch",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-epoch"
|
|
||||||
version = "0.9.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
@ -661,12 +651,6 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.16.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -1623,26 +1607,6 @@ dependencies = [
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
"rayon-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon-core"
|
|
||||||
version = "1.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -1819,8 +1783,8 @@ name = "server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
|
"crossbeam-channel",
|
||||||
"glam 0.27.0",
|
"glam 0.27.0",
|
||||||
"rayon",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_ecs = "0.19"
|
bevy_ecs = "0.19"
|
||||||
|
crossbeam-channel = "0.5.16"
|
||||||
glam = "0.27"
|
glam = "0.27"
|
||||||
rayon = "1.12.0"
|
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
shared = { version = "0.1.0", path = "../shared" }
|
shared = { version = "0.1.0", path = "../shared" }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ fn stream_chunks(
|
||||||
loaded = stats.loaded,
|
loaded = stats.loaded,
|
||||||
unloaded = stats.unloaded,
|
unloaded = stats.unloaded,
|
||||||
resident = stats.resident,
|
resident = stats.resident,
|
||||||
|
in_flight = stats.in_flight,
|
||||||
"streaming reconcile"
|
"streaming reconcile"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
//! Authoritative chunk storage and generation logic for the server.
|
//! Authoritative chunk storage and generation logic for the server.
|
||||||
|
|
||||||
use bevy_ecs::prelude::Resource;
|
use bevy_ecs::prelude::Resource;
|
||||||
use rayon::prelude::*;
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use shared::{
|
use shared::{
|
||||||
generator::VoxelGenerator,
|
generator::VoxelGenerator,
|
||||||
world::{Chunk, ChunkPos},
|
world::{Chunk, ChunkPos},
|
||||||
};
|
};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::Arc,
|
||||||
|
thread::JoinHandle,
|
||||||
|
};
|
||||||
|
|
||||||
/// Outcome of a single streaming reconcile pass, for logging and tests.
|
/// Outcome of a single streaming reconcile pass, for logging and tests.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -19,24 +23,71 @@ pub struct StreamStats {
|
||||||
pub unloaded: usize,
|
pub unloaded: usize,
|
||||||
/// Number of chunks resident after the pass.
|
/// Number of chunks resident after the pass.
|
||||||
pub resident: usize,
|
pub resident: usize,
|
||||||
|
/// Number of chunks in flight.
|
||||||
|
pub in_flight: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The server's authoritative representation of the world.
|
/// The server's authoritative representation of the world.
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct ServerWorld {
|
pub struct ServerWorld {
|
||||||
/// Deterministic voxel source used to generate missing chunks on demand.
|
/// Deterministic voxel source used to generate missing chunks on demand.
|
||||||
generator: VoxelGenerator,
|
generator: Arc<VoxelGenerator>,
|
||||||
/// Currently resident chunks, keyed by chunk-space position.
|
/// Currently resident chunks, keyed by chunk-space position.
|
||||||
chunks: HashMap<ChunkPos, Chunk>,
|
chunks: HashMap<ChunkPos, Chunk>,
|
||||||
|
/// Sending end of the job channel; the main thread pushes positions that require generation.
|
||||||
|
job_tx: Sender<ChunkPos>,
|
||||||
|
/// Receiving end of the result channel; the main thread drains finished chunks returned by workers.
|
||||||
|
result_rx: Receiver<(ChunkPos, Chunk)>,
|
||||||
|
/// Positions dispatched to a worker but not yet returned, preventing the same chunk from being re-dispatched on subsequent passes.
|
||||||
|
in_flight: HashSet<ChunkPos>,
|
||||||
|
/// Handles to the generation worker threads, retained so they can be joined on shutdown.
|
||||||
|
// Retained ahead of a dedicated shutdown path; not yet read because the server has no graceful-stop sequence.
|
||||||
|
#[expect(dead_code)]
|
||||||
|
workers: Vec<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerWorld {
|
impl ServerWorld {
|
||||||
/// Initializes a new authoritative server world with the provided generator.
|
/// Initializes a new authoritative server world with the provided generator.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(generator: VoxelGenerator) -> Self {
|
pub fn new(generator: VoxelGenerator) -> Self {
|
||||||
|
let generator = Arc::new(generator);
|
||||||
|
let (job_tx, job_rx) = crossbeam_channel::unbounded::<ChunkPos>();
|
||||||
|
let (result_tx, result_rx) = crossbeam_channel::unbounded::<(ChunkPos, Chunk)>();
|
||||||
|
|
||||||
|
// One worker per logical core, falling back to a small fixed pool if the platform cannot report its parallelism.
|
||||||
|
let worker_count = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
|
||||||
|
|
||||||
|
let workers = (0..worker_count)
|
||||||
|
.map(|_| {
|
||||||
|
// Each worker owns its own clones: a shared handle to the read-only generator, its own view of the shared job queue, and its own sender back into the result channel.
|
||||||
|
let generator = Arc::clone(&generator);
|
||||||
|
let job_rx = job_rx.clone();
|
||||||
|
let result_tx = result_tx.clone();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// Block until a job arrives.
|
||||||
|
while let Ok(pos) = job_rx.recv() {
|
||||||
|
let chunk = generator.generate_chunk(pos);
|
||||||
|
// A send error means the main thread has gone away; nothing left to do but let the worker wind down.
|
||||||
|
if result_tx.send((pos, chunk)).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Drop the template ends left over after cloning.
|
||||||
|
drop(job_rx);
|
||||||
|
drop(result_tx);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
generator,
|
generator,
|
||||||
chunks: HashMap::new(),
|
chunks: HashMap::new(),
|
||||||
|
job_tx,
|
||||||
|
result_rx,
|
||||||
|
in_flight: HashSet::new(),
|
||||||
|
workers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,9 +104,21 @@ impl ServerWorld {
|
||||||
self.chunks.len()
|
self.chunks.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconciles resident chunks against a desired set: generates the chunks present in `desired` but not resident, and evicts resident chunks absent from `desired`.
|
/// Reconciles resident chunks against a desired set without blocking the caller: finished chunks are drained from the worker pool, resident chunks absent from `desired` are evicted, and still-missing chunks are dispatched to the pool.
|
||||||
pub fn reconcile(&mut self, desired: &HashSet<ChunkPos>) -> StreamStats {
|
pub fn reconcile(&mut self, desired: &HashSet<ChunkPos>) -> StreamStats {
|
||||||
// Evict resident chunks that no anchor wants any more.
|
// Drain: absorb every chunk the workers have finished since the last pass.
|
||||||
|
let mut loaded = 0;
|
||||||
|
while let Ok((pos, chunk)) = self.result_rx.try_recv() {
|
||||||
|
// The position is no longer dispatched now that its chunk has returned.
|
||||||
|
self.in_flight.remove(&pos);
|
||||||
|
// Guard against the eviction race: the anchor may have moved away while this chunk was generating, so a returned chunk is only kept if it is still wanted.
|
||||||
|
if desired.contains(&pos) {
|
||||||
|
self.chunks.insert(pos, chunk);
|
||||||
|
loaded += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evict: drop resident chunks that no anchor wants any more.
|
||||||
let stale: Vec<ChunkPos> = self
|
let stale: Vec<ChunkPos> = self
|
||||||
.chunks
|
.chunks
|
||||||
.keys()
|
.keys()
|
||||||
|
|
@ -66,29 +129,20 @@ impl ServerWorld {
|
||||||
self.chunks.remove(pos);
|
self.chunks.remove(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect the desired positions that are not yet resident.
|
// Dispatch: request generation for every wanted position that is neither resident nor already in flight.
|
||||||
let pending: Vec<ChunkPos> = desired
|
for &pos in desired {
|
||||||
.iter()
|
if !self.chunks.contains_key(&pos) && !self.in_flight.contains(&pos) {
|
||||||
.filter(|pos| !self.chunks.contains_key(pos))
|
self.in_flight.insert(pos);
|
||||||
.copied()
|
// A send error means every worker has shut down; there is nothing useful to do with the position, so the failure is ignored.
|
||||||
.collect();
|
let _ = self.job_tx.send(pos);
|
||||||
|
}
|
||||||
// Generate the missing chunks in parallel
|
|
||||||
let generator = &self.generator;
|
|
||||||
let generated: Vec<(ChunkPos, Chunk)> = pending
|
|
||||||
.par_iter()
|
|
||||||
.map(|&pos| (pos, generator.generate_chunk(pos)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let loaded = generated.len();
|
|
||||||
for (pos, chunk) in generated {
|
|
||||||
self.chunks.insert(pos, chunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamStats {
|
StreamStats {
|
||||||
loaded,
|
loaded,
|
||||||
unloaded: stale.len(),
|
unloaded: stale.len(),
|
||||||
resident: self.chunks.len(),
|
resident: self.chunks.len(),
|
||||||
|
in_flight: self.in_flight.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue