chore: chunk gen should be better now

This commit is contained in:
Alexander Medvedev 2026-04-13 18:18:30 +02:00
parent c06ae2715b
commit 511318ae6b
10 changed files with 359 additions and 246 deletions

15
Cargo.lock generated
View File

@ -2765,6 +2765,7 @@ dependencies = [
"hmac 0.13.0", "hmac 0.13.0",
"libloading", "libloading",
"num-bigint", "num-bigint",
"num_cpus",
"ordered-float", "ordered-float",
"pkcs8 0.11.0-rc.11", "pkcs8 0.11.0-rc.11",
"postcard", "postcard",
@ -2776,7 +2777,8 @@ dependencies = [
"pumpkin-protocol", "pumpkin-protocol",
"pumpkin-util", "pumpkin-util",
"pumpkin-world", "pumpkin-world",
"rand 0.10.0", "rand 0.10.1",
"rayon",
"rsa", "rsa",
"rustc-hash", "rustc-hash",
"rustc_version_runtime", "rustc_version_runtime",
@ -2841,7 +2843,7 @@ dependencies = [
"phf", "phf",
"pumpkin-nbt", "pumpkin-nbt",
"pumpkin-util", "pumpkin-util",
"rand 0.10.0", "rand 0.10.1",
"serde", "serde",
] ]
@ -2960,7 +2962,8 @@ dependencies = [
"pumpkin-data", "pumpkin-data",
"pumpkin-nbt", "pumpkin-nbt",
"pumpkin-util", "pumpkin-util",
"rand 0.10.0", "rand 0.10.1",
"rayon",
"rustc-hash", "rustc-hash",
"ruzstd", "ruzstd",
"serde", "serde",
@ -3019,9 +3022,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.10.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [ dependencies = [
"chacha20", "chacha20",
"getrandom 0.4.2", "getrandom 0.4.2",
@ -3726,7 +3729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.2", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.61.2", "windows-sys 0.61.2",

View File

@ -153,7 +153,7 @@ pumpkin-protocol = { path = "pumpkin-protocol" }
pumpkin-util = { path = "pumpkin-util" } pumpkin-util = { path = "pumpkin-util" }
pumpkin-world = { path = "pumpkin-world" } pumpkin-world = { path = "pumpkin-world" }
quote = "1.0" quote = "1.0"
rand = "0.10.0" rand = "0.10.1"
rsa = "=0.10.0-rc.17" rsa = "=0.10.0-rc.17"
cipher = "0.5.1" cipher = "0.5.1"
rustc-hash = "2.1.2" rustc-hash = "2.1.2"

View File

@ -44,6 +44,7 @@ ruzstd.workspace = true
itertools.workspace = true itertools.workspace = true
enum_dispatch.workspace = true enum_dispatch.workspace = true
rayon.workspace = true
# thread_local.workspace = true # thread_local.workspace = true
# lru.workspace = true # lru.workspace = true

View File

@ -12,6 +12,7 @@ use crate::chunk::io::Dirtiable;
use crate::level::{Level, SyncChunk}; use crate::level::{Level, SyncChunk};
use dashmap::DashMap; use dashmap::DashMap;
use pumpkin_config::lighting::LightingEngineConfig; use pumpkin_config::lighting::LightingEngineConfig;
use pumpkin_data::chunk_gen_settings::GenerationSettings;
use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector2::Vector2;
use slotmap::Key; use slotmap::Key;
use std::cmp::{Ordering, max}; use std::cmp::{Ordering, max};
@ -66,10 +67,14 @@ pub struct GenerationSchedule {
io_lock: IOLock, io_lock: IOLock,
running_task_count: u16, running_task_count: u16,
max_in_flight: u16,
queue_dirty: bool,
recv_chunk: crossfire::compat::MRx<(ChunkPos, RecvChunk)>, recv_chunk: crossfire::compat::MRx<(ChunkPos, RecvChunk)>,
io_read: crossfire::compat::MTx<ChunkPos>, io_read: crossfire::compat::MTx<Vec<ChunkPos>>,
io_write: crossfire::compat::Tx<Vec<(ChunkPos, Chunk)>>, io_write: crossfire::compat::Tx<Vec<(ChunkPos, Chunk)>>,
generate: crossfire::compat::MTx<(ChunkPos, Cache, StagedChunkEnum)>, generate: crossfire::compat::MTx<(ChunkPos, Cache, StagedChunkEnum)>,
send_chunk: crossfire::compat::MTx<(ChunkPos, RecvChunk)>,
gen_pool: Option<Arc<rayon::ThreadPool>>,
listener: Arc<ChunkListener>, listener: Arc<ChunkListener>,
lighting_config: LightingEngineConfig, lighting_config: LightingEngineConfig,
} }
@ -82,6 +87,7 @@ impl GenerationSchedule {
level_channel: Arc<LevelChannel>, level_channel: Arc<LevelChannel>,
listener: Arc<ChunkListener>, listener: Arc<ChunkListener>,
thread_tracker: &mut Vec<thread::JoinHandle<()>>, thread_tracker: &mut Vec<thread::JoinHandle<()>>,
gen_pool: Option<Arc<rayon::ThreadPool>>,
) { ) {
let (send_chunk, recv_chunk) = crossfire::compat::mpmc::unbounded_blocking(); let (send_chunk, recv_chunk) = crossfire::compat::mpmc::unbounded_blocking();
@ -110,6 +116,7 @@ impl GenerationSchedule {
io_lock.clone(), io_lock.clone(),
)); ));
if gen_pool.is_none() {
for i in 0..gen_thread_count { for i in 0..gen_thread_count {
let recv_gen = recv_gen.clone(); let recv_gen = recv_gen.clone();
let send_chunk = send_chunk.clone(); let send_chunk = send_chunk.clone();
@ -124,6 +131,13 @@ impl GenerationSchedule {
thread_tracker.push(handle); thread_tracker.push(handle);
} }
}
let max_in_flight = if gen_pool.is_some() {
(num_cpus::get() * 4) as u16
} else {
gen_thread_count as u16
};
let level_sched = level; let level_sched = level;
let lighting_config = level_sched.lighting_config; let lighting_config = level_sched.lighting_config;
@ -141,10 +155,14 @@ impl GenerationSchedule {
waiting_for_chunks: HashSetType::default(), waiting_for_chunks: HashSetType::default(),
io_lock, io_lock,
running_task_count: 0, running_task_count: 0,
max_in_flight,
queue_dirty: false,
recv_chunk, recv_chunk,
io_read: send_read_io, io_read: send_read_io,
io_write: send_write_io, io_write: send_write_io,
generate: send_gen, generate: send_gen,
send_chunk,
gen_pool,
listener, listener,
chunk_map: Default::default(), chunk_map: Default::default(),
lighting_config, lighting_config,
@ -204,21 +222,21 @@ impl GenerationSchedule {
} }
fn sort_queue(&mut self) { fn sort_queue(&mut self) {
let mut new_queue = BinaryHeap::with_capacity(self.queue.len()); if self.queue.is_empty() {
for i in &self.queue { return;
}
let mut tasks: Vec<_> = self.queue.drain().collect();
for i in &mut tasks {
if let Some(node) = self.graph.nodes.get(i.1) { if let Some(node) = self.graph.nodes.get(i.1) {
new_queue.push(TaskHeapNode( i.0 = Self::calc_priority(
Self::calc_priority(
&self.last_level, &self.last_level,
&self.last_high_priority, &self.last_high_priority,
node.pos, node.pos,
node.stage, node.stage,
), );
i.1,
));
} }
} }
self.queue = new_queue; self.queue = BinaryHeap::from(tasks);
} }
/// Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at /// Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at
@ -395,9 +413,9 @@ impl GenerationSchedule {
} }
if let Some(high_priority) = new_data.1 { if let Some(high_priority) = new_data.1 {
self.last_high_priority = high_priority; self.last_high_priority = high_priority;
self.queue_dirty = true;
} }
let Some(new_level) = new_data.0 else { let Some(new_level) = new_data.0 else {
self.sort_queue();
return true; return true;
}; };
for (pos, (old_stage, new_stage)) in new_level.0 { for (pos, (old_stage, new_stage)) in new_level.0 {
@ -503,7 +521,7 @@ impl GenerationSchedule {
self.chunk_map.insert(pos, holder); self.chunk_map.insert(pos, holder);
} }
self.last_level = new_level.1; self.last_level = new_level.1;
self.sort_queue(); self.queue_dirty = true;
true true
} }
@ -915,6 +933,22 @@ impl GenerationSchedule {
break; break;
} }
// 1. Get latest world state (player moves, etc)
self.resort_work(self.send_level.get());
// 2. Process all pending chunk results from workers
while let Ok((pos, data)) = self.recv_chunk.try_recv() {
self.receive_chunk(pos, data);
}
// 3. Re-sort if world state changed or new tasks added
if self.queue_dirty {
self.sort_queue();
self.queue_dirty = false;
}
// 4. Process ready tasks in the queue (up to max_in_flight)
let mut io_batch = Vec::with_capacity(16);
'out2: while let Some(task) = self.queue.pop() { 'out2: while let Some(task) = self.queue.pop() {
if level.shut_down_chunk_system.load(Relaxed) { if level.shut_down_chunk_system.load(Relaxed) {
self.queue.push(task); self.queue.push(task);
@ -923,13 +957,22 @@ impl GenerationSchedule {
break 'out2; break 'out2;
} }
if self.resort_work(self.send_level.get()) { if self.running_task_count >= self.max_in_flight {
self.queue.push(task); self.queue.push(task);
break 'out2; break 'out2;
} }
// Briefly check for high-priority results or world changes to avoid stalling
while let Ok((pos, data)) = self.recv_chunk.try_recv() { while let Ok((pos, data)) = self.recv_chunk.try_recv() {
self.receive_chunk(pos, data); self.receive_chunk(pos, data);
if self.resort_work(self.send_level.get()) {
// If world state changed, we MUST re-sort before continuing
self.queue.push(task);
self.queue_dirty = true;
break 'out2;
} }
}
if let Some(node) = self.graph.nodes.get_mut(task.1) { if let Some(node) = self.graph.nodes.get_mut(task.1) {
if node.in_degree != 0 { if node.in_degree != 0 {
node.in_queue = false; node.in_queue = false;
@ -951,12 +994,24 @@ impl GenerationSchedule {
} }
holder.occupied = occupy; holder.occupied = occupy;
if self.io_read.send(node.pos).is_err() { io_batch.push(node.pos);
if io_batch.len() >= 16
&& self.io_read.send(std::mem::take(&mut io_batch)).is_err()
{
info!("IO read thread closed, saving remaining chunks..."); info!("IO read thread closed, saving remaining chunks...");
self.save_all_chunk(true); self.save_all_chunk(true);
break 'out2; break 'out2;
} }
} else { } else {
// Send any pending IO batch before starting generation
if !io_batch.is_empty()
&& self.io_read.send(std::mem::take(&mut io_batch)).is_err()
{
info!("IO read thread closed, saving remaining chunks...");
self.save_all_chunk(true);
break 'out2;
}
let write_radius = node.stage.get_write_radius(); let write_radius = node.stage.get_write_radius();
// Pre-validate that every chunk in the write area (including the // Pre-validate that every chunk in the write area (including the
@ -1058,7 +1113,21 @@ impl GenerationSchedule {
} }
self.running_task_count += 1; self.running_task_count += 1;
if self.generate.send((node.pos, cache, node.stage)).is_err() { if let Some(pool) = &self.gen_pool {
let pos = node.pos;
let stage = node.stage;
let send_chunk = self.send_chunk.clone();
let level = level.clone();
let settings =
GenerationSettings::from_dimension(&level.world_gen.dimension);
pool.spawn(move || {
let result = crate::chunk_system::worker_logic::run_generation(
pos, cache, stage, &level, settings,
);
let _ = send_chunk.send((pos, result));
});
} else if self.generate.send((node.pos, cache, node.stage)).is_err() {
self.running_task_count = self.running_task_count.saturating_sub(1); self.running_task_count = self.running_task_count.saturating_sub(1);
info!("Generation thread closed, saving remaining chunks..."); info!("Generation thread closed, saving remaining chunks...");
self.save_all_chunk(true); self.save_all_chunk(true);
@ -1068,25 +1137,40 @@ impl GenerationSchedule {
} }
} }
// Flush any remaining IO batch
if !io_batch.is_empty() && self.io_read.send(std::mem::take(&mut io_batch)).is_err() {
info!("IO read thread closed, saving remaining chunks...");
self.save_all_chunk(true);
}
// 3. If queue is empty, wait for work or results
if self.queue.is_empty() { if self.queue.is_empty() {
// Wait while there are in-flight tasks OR tasks parked waiting for chunk data. // If we have tasks in flight, wait for them with timeout
while (self.running_task_count > 0 || !self.waiting_for_chunks.is_empty()) if self.running_task_count > 0 || !self.waiting_for_chunks.is_empty() {
&& self.queue.is_empty() match self.recv_chunk.recv_timeout(Duration::from_millis(5)) {
{ Ok((pos, data)) => {
if let Ok((pos, data)) = self.recv_chunk.try_recv() {
self.receive_chunk(pos, data); self.receive_chunk(pos, data);
self.resort_work(self.send_level.get()); self.resort_work(self.send_level.get());
}
Err(crossfire::compat::RecvTimeoutError::Timeout) => {
// Periodically check LevelChannel for new requests
self.resort_work(self.send_level.get());
}
Err(crossfire::compat::RecvTimeoutError::Disconnected) => break,
}
if self.queue_dirty {
self.sort_queue();
self.queue_dirty = false;
}
} else { } else {
if level.shut_down_chunk_system.load(Relaxed) { // No tasks in flight, wait indefinitely for LevelChannel changes
break;
}
thread::sleep(Duration::from_millis(50));
}
}
if self.queue.is_empty() && self.waiting_for_chunks.is_empty() {
debug_assert!(self.debug_check()); debug_assert!(self.debug_check());
debug_assert_eq!(self.running_task_count, 0); debug_assert_eq!(self.running_task_count, 0);
self.resort_work(self.send_level.wait_and_get(&level)); self.resort_work(self.send_level.wait_and_get(&level));
if self.queue_dirty {
self.sort_queue();
self.queue_dirty = false;
}
} }
} }
} }

View File

@ -55,7 +55,7 @@ fn needs_relighting(chunk: &crate::chunk::ChunkData, config: &LightingEngineConf
} }
pub async fn io_read_work( pub async fn io_read_work(
recv: crossfire::compat::MAsyncRx<ChunkPos>, recv: crossfire::compat::MAsyncRx<Vec<ChunkPos>>,
send: crossfire::compat::MTx<(ChunkPos, RecvChunk)>, send: crossfire::compat::MTx<(ChunkPos, RecvChunk)>,
level: Arc<Level>, level: Arc<Level>,
lock: IOLock, lock: IOLock,
@ -64,23 +64,26 @@ pub async fn io_read_work(
debug!("io read thread start"); debug!("io read thread start");
let biome_mixer_seed = hash_seed(level.world_gen.random_config.seed); let biome_mixer_seed = hash_seed(level.world_gen.random_config.seed);
let dimension = &level.world_gen.dimension; let dimension = &level.world_gen.dimension;
let (t_send, mut t_recv) = tokio::sync::mpsc::channel(1); let (t_send, mut t_recv) = tokio::sync::mpsc::channel(16);
// Cleaner loop and async recv // Cleaner loop and async recv
while let Ok(pos) = recv.recv().await { while let Ok(batch) = recv.recv().await {
for pos in &batch {
// Lock handling // Lock handling
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
let mut data = lock.0.lock().unwrap(); let mut data = lock.0.lock().unwrap();
while data.contains_key(&pos) { while data.contains_key(pos) {
data = lock.1.wait(data).unwrap(); data = lock.1.wait(data).unwrap();
} }
}); });
}
level level
.chunk_saver .chunk_saver
.fetch_chunks(&level.level_folder, &[pos], t_send.clone()) .fetch_chunks(&level.level_folder, &batch, t_send.clone())
.await; .await;
for _ in 0..batch.len() {
let data = match t_recv.recv().await { let data = match t_recv.recv().await {
Some(res) => res, Some(res) => res,
None => break, None => break,
@ -88,6 +91,7 @@ pub async fn io_read_work(
match data { match data {
Loaded(chunk) => { Loaded(chunk) => {
let pos = ChunkPos::new(chunk.x, chunk.z);
if chunk.status == ChunkStatus::Full { if chunk.status == ChunkStatus::Full {
// Relighting check // Relighting check
let needs_relight = needs_relighting(&chunk, &level.lighting_config); let needs_relight = needs_relighting(&chunk, &level.lighting_config);
@ -108,7 +112,7 @@ pub async fn io_read_work(
// Clear all lighting data // Clear all lighting data
let section_count = proto.light.sky_light.len(); let section_count = proto.light.sky_light.len();
proto.light.sky_light = (0..section_count) proto.light.sky_light = (0..section_count)
.map(|_| LightContainer::new_empty(0)) .map(|_| LightContainer::new_empty(15))
.collect(); .collect();
proto.light.block_light = (0..section_count) proto.light.block_light = (0..section_count)
.map(|_| LightContainer::new_empty(0)) .map(|_| LightContainer::new_empty(0))
@ -134,7 +138,8 @@ pub async fn io_read_work(
} }
} else { } else {
// Standard ProtoChunk handling for non-full chunks // Standard ProtoChunk handling for non-full chunks
let val = RecvChunk::IO(Chunk::Proto(Box::new(ProtoChunk::from_chunk_data( let val =
RecvChunk::IO(Chunk::Proto(Box::new(ProtoChunk::from_chunk_data(
&chunk, &chunk,
dimension, dimension,
level.world_gen.default_block, level.world_gen.default_block,
@ -144,15 +149,8 @@ pub async fn io_read_work(
break; break;
} }
} }
continue;
} }
LoadedData::Missing(_) => {} LoadedData::Missing(pos) | LoadedData::Error((pos, _)) => {
LoadedData::Error(_) => {
warn!("chunk data read error pos: {pos:?}. regenerating");
}
}
// Final send for missing/error cases (regenerate)
if send if send
.send(( .send((
pos, pos,
@ -169,6 +167,9 @@ pub async fn io_read_work(
break; break;
} }
} }
}
}
}
debug!("io read thread stop"); debug!("io read thread stop");
} }
@ -226,21 +227,13 @@ pub async fn io_write_work(recv: AsyncRx<Vec<(ChunkPos, Chunk)>>, level: Arc<Lev
} }
} }
pub fn generation_work( pub fn run_generation(
recv: crossfire::compat::MRx<(ChunkPos, Cache, StagedChunkEnum)>, pos: ChunkPos,
send: crossfire::compat::MTx<(ChunkPos, RecvChunk)>, mut cache: Cache,
level: Arc<Level>, stage: StagedChunkEnum,
) { level: &Level,
let settings = GenerationSettings::from_dimension(&level.world_gen.dimension); settings: &GenerationSettings,
) -> RecvChunk {
loop {
let (pos, mut cache, stage) = if let Ok(data) = recv.recv() {
data
} else {
debug!("generation channel closed, exiting");
break;
};
// Run generation with panic catching // Run generation with panic catching
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cache.advance( cache.advance(
@ -258,11 +251,7 @@ pub fn generation_work(
})); }));
match result { match result {
Ok(cache) => { Ok(cache) => RecvChunk::Generation(cache),
if send.send((pos, RecvChunk::Generation(cache))).is_err() {
break;
}
}
Err(payload) => { Err(payload) => {
let msg = payload let msg = payload
.downcast_ref::<&str>() .downcast_ref::<&str>()
@ -276,16 +265,33 @@ pub fn generation_work(
error!("Chunk generation FAILED at {pos:?} ({stage:?}): {msg}"); error!("Chunk generation FAILED at {pos:?} ({stage:?}): {msg}");
// Send failure notification
let _ = send.send((
pos,
RecvChunk::GenerationFailure { RecvChunk::GenerationFailure {
pos, pos,
stage, stage,
error: msg.to_string(), error: msg.to_string(),
},
));
} }
} }
} }
} }
pub fn generation_work(
recv: crossfire::compat::MRx<(ChunkPos, Cache, StagedChunkEnum)>,
send: crossfire::compat::MTx<(ChunkPos, RecvChunk)>,
level: Arc<Level>,
) {
let settings = GenerationSettings::from_dimension(&level.world_gen.dimension);
loop {
let (pos, cache, stage) = if let Ok(data) = recv.recv() {
data
} else {
debug!("generation channel closed, exiting");
break;
};
let result = run_generation(pos, cache, stage, &level, settings);
if send.send((pos, result)).is_err() {
break;
}
}
}

View File

@ -11,6 +11,7 @@ pub fn into_level(
mut base_directory: PathBuf, mut base_directory: PathBuf,
block_registry: Arc<dyn BlockRegistryExt>, block_registry: Arc<dyn BlockRegistryExt>,
seed: i64, seed: i64,
gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Level> { ) -> Arc<Level> {
if dimension == Dimension::OVERWORLD { if dimension == Dimension::OVERWORLD {
} else if dimension == Dimension::THE_NETHER { } else if dimension == Dimension::THE_NETHER {
@ -24,5 +25,6 @@ pub fn into_level(
block_registry, block_registry,
seed, seed,
dimension, dimension,
gen_pool,
) )
} }

View File

@ -14,7 +14,6 @@ use crate::{
tick::{OrderedTick, ScheduledTick, TickPriority}, tick::{OrderedTick, ScheduledTick, TickPriority},
world::BlockRegistryExt, world::BlockRegistryExt,
}; };
use crossbeam::channel::Sender;
use dashmap::{DashMap, Entry}; use dashmap::{DashMap, Entry};
use pumpkin_config::{chunk::ChunkConfig, lighting::LightingEngineConfig, world::LevelConfig}; use pumpkin_config::{chunk::ChunkConfig, lighting::LightingEngineConfig, world::LevelConfig};
use pumpkin_data::biome::Biome; use pumpkin_data::biome::Biome;
@ -96,12 +95,12 @@ pub struct Level {
/// Number of ticks between autosave checks. If 0, autosave is disabled. /// Number of ticks between autosave checks. If 0, autosave is disabled.
pub autosave_ticks: u64, pub autosave_ticks: u64,
gen_entity_request_tx: Sender<Vector2<i32>>,
pending_entity_generations: Arc<DashMap<Vector2<i32>, Vec<oneshot::Sender<SyncEntityChunk>>>>, pending_entity_generations: Arc<DashMap<Vector2<i32>, Vec<oneshot::Sender<SyncEntityChunk>>>>,
pub level_channel: Arc<LevelChannel>, pub level_channel: Arc<LevelChannel>,
pub thread_tracker: Mutex<Vec<thread::JoinHandle<()>>>, pub thread_tracker: Mutex<Vec<thread::JoinHandle<()>>>,
pub chunk_listener: Arc<ChunkListener>, pub chunk_listener: Arc<ChunkListener>,
pub gen_pool: Option<Arc<rayon::ThreadPool>>,
} }
pub struct TickData { pub struct TickData {
@ -145,6 +144,7 @@ impl Level {
block_registry: Arc<dyn BlockRegistryExt>, block_registry: Arc<dyn BlockRegistryExt>,
seed: i64, seed: i64,
dimension: Dimension, dimension: Dimension,
gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Self> { ) -> Arc<Self> {
let region_folder = root_folder.join("region"); let region_folder = root_folder.join("region");
let entities_folder = root_folder.join("entities"); let entities_folder = root_folder.join("entities");
@ -178,7 +178,6 @@ impl Level {
>::new(config.clone())), >::new(config.clone())),
}; };
let (gen_entity_request_tx, gen_entity_request_rx) = crossbeam::channel::unbounded();
let pending_entity_generations = Arc::new(DashMap::new()); let pending_entity_generations = Arc::new(DashMap::new());
let level_channel = Arc::new(LevelChannel::new()); let level_channel = Arc::new(LevelChannel::new());
let thread_tracker = Mutex::new(Vec::new()); let thread_tracker = Mutex::new(Vec::new());
@ -205,44 +204,55 @@ impl Level {
should_save: AtomicBool::new(false), should_save: AtomicBool::new(false),
should_unload: AtomicBool::new(false), should_unload: AtomicBool::new(false),
autosave_ticks: level_config.autosave_ticks, autosave_ticks: level_config.autosave_ticks,
gen_entity_request_tx,
pending_entity_generations: pending_entity_generations.clone(), pending_entity_generations: pending_entity_generations.clone(),
level_channel: level_channel.clone(), level_channel: level_channel.clone(),
thread_tracker, thread_tracker,
chunk_listener: listener.clone(), chunk_listener: listener.clone(),
gen_pool: gen_pool.clone(),
}); });
// TODO // TODO
let total_cores = num_cpus::get().saturating_sub(2).max(1); let total_cores = num_cpus::get().saturating_sub(2).max(1);
let threads_per_dimension = (total_cores / 2).max(1); let threads_per_dimension = (total_cores / 2).max(1);
let entity_threads = 1;
GenerationSchedule::create( GenerationSchedule::create(
2, 4,
threads_per_dimension, threads_per_dimension,
level_ref.clone(), level_ref.clone(),
level_channel, level_channel,
listener, listener,
level_ref.thread_tracker.lock().unwrap().as_mut(), level_ref.thread_tracker.lock().unwrap().as_mut(),
gen_pool,
); );
let mut tracker_lock = level_ref.thread_tracker.lock().unwrap(); level_ref
for thread_id in 0..entity_threads {
let level_clone = level_ref.clone();
let pending_clone = pending_entity_generations.clone();
let rx = gen_entity_request_rx.clone();
let handle = thread::Builder::new()
.name(format!("Entity Chunk Generation Thread {thread_id}"))
.spawn(move || {
loop {
if level_clone.cancel_token.is_cancelled() {
break;
} }
match rx.recv_timeout(std::time::Duration::from_millis(500)) { pub fn spawn_entity_generation(self: &Arc<Self>, pos: Vector2<i32>) {
Ok(pos) => { let level = self.clone();
if let Some(pool) = &self.gen_pool {
pool.spawn(move || {
let arc_chunk = Arc::new(ChunkEntityData {
x: pos.x,
z: pos.y,
data: tokio::sync::Mutex::new(FxHashMap::default()),
dirty: AtomicBool::new(true),
});
level.loaded_entity_chunks.insert(pos, arc_chunk.clone());
if let Some((_, waiters)) = level.pending_entity_generations.remove(&pos) {
for tx in waiters {
let _ = tx.send(arc_chunk.clone());
}
}
});
} else {
// Fallback to spawning a new thread if no pool is available (should not happen in production)
let level_clone = level.clone();
thread::Builder::new()
.name(format!("Entity Gen {:?}", pos))
.spawn(move || {
let arc_chunk = Arc::new(ChunkEntityData { let arc_chunk = Arc::new(ChunkEntityData {
x: pos.x, x: pos.x,
z: pos.y, z: pos.y,
@ -254,24 +264,15 @@ impl Level {
.loaded_entity_chunks .loaded_entity_chunks
.insert(pos, arc_chunk.clone()); .insert(pos, arc_chunk.clone());
if let Some((_, waiters)) = pending_clone.remove(&pos) { if let Some((_, waiters)) = level_clone.pending_entity_generations.remove(&pos)
{
for tx in waiters { for tx in waiters {
let _ = tx.send(arc_chunk.clone()); let _ = tx.send(arc_chunk.clone());
} }
} }
}
Err(crossbeam::channel::RecvTimeoutError::Timeout) => continue,
Err(crossbeam::channel::RecvTimeoutError::Disconnected) => break,
}
}
}) })
.expect("Failed to spawn Entity Generation Thread"); .expect("Failed to spawn entity generation thread");
tracker_lock.push(handle);
} }
drop(tracker_lock);
level_ref
} }
/// Spawns a task associated with this world. All tasks spawned with this method are awaited /// Spawns a task associated with this world. All tasks spawned with this method are awaited
@ -645,7 +646,7 @@ impl Level {
} }
dashmap::mapref::entry::Entry::Vacant(entry) => { dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert(vec![tx]); entry.insert(vec![tx]);
let _ = level_clone.gen_entity_request_tx.send(pos); level_clone.spawn_entity_generation(pos);
} }
} }
if let Ok(chunk) = rx.await { if let Ok(chunk) = rx.await {
@ -683,7 +684,7 @@ impl Level {
} }
dashmap::mapref::entry::Entry::Vacant(entry) => { dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert(vec![tx]); entry.insert(vec![tx]);
let _ = self.gen_entity_request_tx.send(pos); self.spawn_entity_generation(pos);
} }
} }
rx.await.expect("Entity generation worker dropped") rx.await.expect("Entity generation worker dropped")

View File

@ -19,6 +19,8 @@ doctest = false
[dependencies] [dependencies]
# pumpkin # pumpkin
arc-swap.workspace = true arc-swap.workspace = true
rayon.workspace = true
num_cpus.workspace = true
pumpkin-util.workspace = true pumpkin-util.workspace = true
pumpkin-nbt.workspace = true pumpkin-nbt.workspace = true
pumpkin-config.workspace = true pumpkin-config.workspace = true

View File

@ -109,8 +109,6 @@ const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20
pub const DATA_VERSION: i32 = 4786; // 26.1 pub const DATA_VERSION: i32 = 4786; // 26.1
enum BatchState { enum BatchState {
Initial,
Waiting,
Count(u8), Count(u8),
} }
@ -168,7 +166,7 @@ impl ChunkManager {
chunk_sent: HashMap::new(), chunk_sent: HashMap::new(),
chunk_queue: BinaryHeap::new(), chunk_queue: BinaryHeap::new(),
entity_chunk_queue: VecDeque::new(), entity_chunk_queue: VecDeque::new(),
batches_sent_since_ack: BatchState::Initial, batches_sent_since_ack: BatchState::Count(0),
last_chunk_batch_sent_at: Instant::now(), last_chunk_batch_sent_at: Instant::now(),
world, world,
} }
@ -191,8 +189,6 @@ impl ChunkManager {
const fn ack_window_open(&self) -> bool { const fn ack_window_open(&self) -> bool {
match self.batches_sent_since_ack { match self.batches_sent_since_ack {
BatchState::Count(count) => count < Self::NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE, BatchState::Count(count) => count < Self::NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE,
BatchState::Initial => true,
BatchState::Waiting => false,
} }
} }
@ -203,7 +199,7 @@ impl ChunkManager {
} }
pub fn pull_new_chunks(&mut self) { pub fn pull_new_chunks(&mut self) {
// log::debug!("pull new chunks"); let mut new_nodes = Vec::new();
while let Ok((pos, chunk)) = self.chunk_listener.try_recv() { while let Ok((pos, chunk)) = self.chunk_listener.try_recv() {
let dst = (pos.x - self.center.x) let dst = (pos.x - self.center.x)
.abs() .abs()
@ -212,12 +208,19 @@ impl ChunkManager {
continue; continue;
} }
if self.should_enqueue_chunk(pos, &chunk) { if self.should_enqueue_chunk(pos, &chunk) {
// log::debug!("receive new chunk {pos:?}"); new_nodes.push(HeapNode(dst, pos, chunk));
self.chunk_queue.push(HeapNode(dst, pos, chunk)); }
}
if !new_nodes.is_empty() {
if self.chunk_queue.is_empty() {
self.chunk_queue = BinaryHeap::from(new_nodes);
} else {
for node in new_nodes {
self.chunk_queue.push(node);
}
} }
} }
// log::debug!("chunk_queue size {}", self.chunk_queue.len());
// log::debug!("chunk_sent size {}", self.chunk_sent.len());
} }
pub fn update_center_and_view_distance( pub fn update_center_and_view_distance(
@ -257,23 +260,28 @@ impl ChunkManager {
&& !unloading_chunks.contains(pos) && !unloading_chunks.contains(pos)
}); });
let mut new_queue = BinaryHeap::with_capacity(self.chunk_queue.len()); let mut tasks: Vec<_> = self.chunk_queue.drain().collect();
for node in self.chunk_queue.drain() { tasks.retain(|node| {
let dst = (node.1.x - center.x).abs().max((node.1.y - center.y).abs()); let dst = (node.1.x - center.x).abs().max((node.1.y - center.y).abs());
if dst <= view_distance_i32 && !unloading_chunks.contains(&node.1) { dst <= view_distance_i32 && !unloading_chunks.contains(&node.1)
new_queue.push(HeapNode(dst, node.1, node.2)); });
for node in &mut tasks {
node.0 = (node.1.x - center.x).abs().max((node.1.y - center.y).abs());
} }
}
self.chunk_queue = new_queue;
for pos in loading_chunks { for pos in loading_chunks {
if !self.chunk_sent.contains_key(pos) if !self.chunk_sent.contains_key(pos)
&& let Some(chunk) = level.loaded_chunks.get(pos) && let Some(chunk) = level.loaded_chunks.get(pos)
{ {
self.push_chunk(*pos, chunk.value().clone()); let chunk = chunk.value().clone();
if self.should_enqueue_chunk(*pos, &chunk) {
let dst = (pos.x - center.x).abs().max((pos.y - center.y).abs());
tasks.push(HeapNode(dst, *pos, chunk));
} }
} }
} }
self.chunk_queue = BinaryHeap::from(tasks);
}
pub fn clean_up(&mut self, level: &Arc<Level>) { pub fn clean_up(&mut self, level: &Arc<Level>) {
let mut lock = level.chunk_loading.lock().unwrap(); let mut lock = level.chunk_loading.lock().unwrap();
@ -290,7 +298,7 @@ impl ChunkManager {
self.chunk_sent.clear(); self.chunk_sent.clear();
self.chunk_queue.clear(); self.chunk_queue.clear();
self.entity_chunk_queue.clear(); self.entity_chunk_queue.clear();
self.batches_sent_since_ack = BatchState::Initial; self.batches_sent_since_ack = BatchState::Count(0);
self.last_chunk_batch_sent_at = Instant::now(); self.last_chunk_batch_sent_at = Instant::now();
} }
@ -307,7 +315,7 @@ impl ChunkManager {
self.chunk_queue.clear(); self.chunk_queue.clear();
self.world = new_world; self.world = new_world;
// Reset batch state so chunks can be sent immediately in the new dimension // Reset batch state so chunks can be sent immediately in the new dimension
self.batches_sent_since_ack = BatchState::Initial; self.batches_sent_since_ack = BatchState::Count(0);
self.last_chunk_batch_sent_at = Instant::now(); self.last_chunk_batch_sent_at = Instant::now();
} }
@ -347,8 +355,6 @@ impl ChunkManager {
BatchState::Count(count) => { BatchState::Count(count) => {
*count = count.saturating_add(1); *count = count.saturating_add(1);
} }
state @ BatchState::Initial => *state = BatchState::Waiting,
BatchState::Waiting => (),
} }
self.last_chunk_batch_sent_at = Instant::now(); self.last_chunk_batch_sent_at = Instant::now();
@ -371,8 +377,6 @@ impl ChunkManager {
BatchState::Count(count) => { BatchState::Count(count) => {
*count = count.saturating_add(1); *count = count.saturating_add(1);
} }
state @ BatchState::Initial => *state = BatchState::Waiting,
BatchState::Waiting => (),
} }
self.last_chunk_batch_sent_at = Instant::now(); self.last_chunk_batch_sent_at = Instant::now();

View File

@ -254,6 +254,15 @@ impl Server {
}; };
let server = Arc::new(server); let server = Arc::new(server);
let total_cores = num_cpus::get().saturating_sub(2).max(1);
let gen_pool = Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(total_cores)
.thread_name(|i| format!("Gen-Pool-{i}"))
.build()
.expect("Failed to build generation thread pool"),
);
let server_clone = server.clone(); let server_clone = server.clone();
tokio::spawn(async move { tokio::spawn(async move {
server_clone server_clone
@ -268,6 +277,7 @@ impl Server {
let l_info = server.level_info.clone(); // Access from struct let l_info = server.level_info.clone(); // Access from struct
let weak = Arc::downgrade(&server); let weak = Arc::downgrade(&server);
let config = Arc::new(server.advanced_config.world.clone()); let config = Arc::new(server.advanced_config.world.clone());
let pool = gen_pool.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
info!( info!(
@ -277,7 +287,7 @@ impl Server {
.to_pretty_console() .to_pretty_console()
); );
World::load( World::load(
into_level(dim, &config, path, registry.clone(), seed), into_level(dim, &config, path, registry.clone(), seed, Some(pool)),
l_info, l_info,
dim, dim,
registry, registry,