From d41e5b9b98144ec60fb72d110c1df377d3267a0c Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 6 Mar 2026 22:01:39 +0100 Subject: [PATCH] fix: structures --- pumpkin-util/src/noise/perlin.rs | 2 +- pumpkin-util/src/random/xoroshiro128.rs | 39 +- pumpkin-world/benches/chunk.rs | 2 +- pumpkin-world/benches/noise_router.rs | 2 +- pumpkin-world/src/biome/mod.rs | 4 +- pumpkin-world/src/biome/multi_noise.rs | 4 +- .../src/chunk_system/chunk_holder.rs | 6 + pumpkin-world/src/chunk_system/chunk_state.rs | 16 +- pumpkin-world/src/chunk_system/schedule.rs | 401 +++++++++++------- pumpkin-world/src/generation/generator/mod.rs | 2 +- pumpkin-world/src/generation/mod.rs | 19 +- .../src/generation/noise/aquifer_sampler.rs | 6 +- pumpkin-world/src/generation/noise/mod.rs | 25 +- .../src/generation/noise/ore_sampler.rs | 16 +- pumpkin-world/src/generation/noise/perlin.rs | 8 +- .../noise/router/chunk_density_function.rs | 6 +- .../noise/router/density_function/noise.rs | 4 +- .../noise/router/density_function/test.rs | 2 +- .../noise/router/multi_noise_sampler.rs | 4 +- .../noise/router/proto_noise_router.rs | 16 +- pumpkin-world/src/generation/proto_chunk.rs | 11 +- pumpkin-world/src/generation/surface/mod.rs | 6 +- .../src/generation/surface/terrain.rs | 16 +- pumpkin-world/src/lib.rs | 4 +- 24 files changed, 374 insertions(+), 247 deletions(-) diff --git a/pumpkin-util/src/noise/perlin.rs b/pumpkin-util/src/noise/perlin.rs index eee3c6eb5..909535220 100644 --- a/pumpkin-util/src/noise/perlin.rs +++ b/pumpkin-util/src/noise/perlin.rs @@ -477,7 +477,7 @@ mod tests { use crate::{ assert_eq_delta, noise::perlin::{OctavePerlinNoiseSampler, PerlinNoiseSampler}, - random::{RandomDeriverImpl, RandomImpl, legacy_rand::LegacyRand, xoroshiro128::Xoroshiro}, + random::{RandomImpl, legacy_rand::LegacyRand, xoroshiro128::Xoroshiro}, read_data_from_file, }; diff --git a/pumpkin-util/src/random/xoroshiro128.rs b/pumpkin-util/src/random/xoroshiro128.rs index 084dee4c9..71d9d6969 100644 --- a/pumpkin-util/src/random/xoroshiro128.rs +++ b/pumpkin-util/src/random/xoroshiro128.rs @@ -126,6 +126,13 @@ impl Xoroshiro { self.hi = m.rotate_left(28); n } + + pub const fn next_splitter(&mut self) -> XoroshiroSplitter { + XoroshiroSplitter { + lo: self.next_random(), + hi: self.next_random(), + } + } } impl GaussianGenerator for Xoroshiro { @@ -160,10 +167,7 @@ impl RandomImpl for Xoroshiro { } fn next_splitter(&mut self) -> RandomDeriver { - RandomDeriver::Xoroshiro(XoroshiroSplitter { - lo: self.next_random(), - hi: self.next_random(), - }) + RandomDeriver::Xoroshiro(self.next_splitter()) } fn next_i32(&mut self) -> i32 { @@ -220,25 +224,36 @@ pub struct XoroshiroSplitter { hi: u64, } -impl RandomDeriverImpl for XoroshiroSplitter { - fn split_string(&self, seed: &str) -> RandomGenerator { +impl XoroshiroSplitter { + #[must_use] + pub fn split_string(&self, seed: &str) -> Xoroshiro { let bytes = md5::compute(seed.as_bytes()); let l = u64::from_be_bytes(bytes[0..8].try_into().expect("incorrect length")); let m = u64::from_be_bytes(bytes[8..16].try_into().expect("incorrect length")); - RandomGenerator::Xoroshiro(Xoroshiro::new(l ^ self.lo, m ^ self.hi)) + Xoroshiro::new(l ^ self.lo, m ^ self.hi) + } + + #[must_use] + pub fn split_pos(&self, x: i32, y: i32, z: i32) -> Xoroshiro { + let l = hash_block_pos(x, y, z) as u64; + let m = l ^ self.lo; + + Xoroshiro::new(m, self.hi) + } +} + +impl RandomDeriverImpl for XoroshiroSplitter { + fn split_string(&self, seed: &str) -> RandomGenerator { + RandomGenerator::Xoroshiro(self.split_string(seed)) } fn split_u64(&self, seed: u64) -> RandomGenerator { RandomGenerator::Xoroshiro(Xoroshiro::new(seed ^ self.lo, seed ^ self.hi)) } - #[expect(clippy::many_single_char_names)] fn split_pos(&self, x: i32, y: i32, z: i32) -> RandomGenerator { - let l = hash_block_pos(x, y, z) as u64; - let m = l ^ self.lo; - - RandomGenerator::Xoroshiro(Xoroshiro::new(m, self.hi)) + RandomGenerator::Xoroshiro(self.split_pos(x, y, z)) } } diff --git a/pumpkin-world/benches/chunk.rs b/pumpkin-world/benches/chunk.rs index d25da0747..4d74953c3 100644 --- a/pumpkin-world/benches/chunk.rs +++ b/pumpkin-world/benches/chunk.rs @@ -11,7 +11,7 @@ use pumpkin_world::{ fn bench_terrain_gen(c: &mut Criterion) { let seed = 0; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let base_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); let surface_config = GenerationSettings::from_dimension(&Dimension::OVERWORLD); let terrain_cache = TerrainCache::from_random(&random_config); diff --git a/pumpkin-world/benches/noise_router.rs b/pumpkin-world/benches/noise_router.rs index 15f22731d..755de931d 100644 --- a/pumpkin-world/benches/noise_router.rs +++ b/pumpkin-world/benches/noise_router.rs @@ -11,7 +11,7 @@ use std::hint::black_box; fn bench_noise_router_creation(c: &mut Criterion) { let base_routers = &OVERWORLD_BASE_NOISE_ROUTER; - let random_config = GlobalRandomConfig::new(0, false); + let random_config = GlobalRandomConfig::new(0); let proto_routers = ProtoNoiseRouters::generate(base_routers, &random_config); let proto_noise_router = proto_routers.noise; diff --git a/pumpkin-world/src/biome/mod.rs b/pumpkin-world/src/biome/mod.rs index 669f37ea2..5736ce19a 100644 --- a/pumpkin-world/src/biome/mod.rs +++ b/pumpkin-world/src/biome/mod.rs @@ -73,7 +73,7 @@ mod test { #[test] fn biome_desert() { let seed = 13579; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let noise_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); let multi_noise_config = MultiNoiseSamplerBuilderOptions::new(1, 1, 1); @@ -101,7 +101,7 @@ mod test { read_data_from_file!("../../assets/biome_no_blend_no_beard_0.json"); let seed = 0; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let noise_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); let surface_settings = GenerationSettings::from_dimension(&Dimension::OVERWORLD); diff --git a/pumpkin-world/src/biome/multi_noise.rs b/pumpkin-world/src/biome/multi_noise.rs index 88163d328..8fe1f1a0e 100644 --- a/pumpkin-world/src/biome/multi_noise.rs +++ b/pumpkin-world/src/biome/multi_noise.rs @@ -66,7 +66,7 @@ mod test { let chunk_x = 0; let chunk_z = 0; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let noise_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); @@ -112,7 +112,7 @@ mod test { read_data_from_file!("../../assets/multi_noise_biome_source_test.json"); let seed = 0; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let noise_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); diff --git a/pumpkin-world/src/chunk_system/chunk_holder.rs b/pumpkin-world/src/chunk_system/chunk_holder.rs index cbefb1c7b..383648540 100644 --- a/pumpkin-world/src/chunk_system/chunk_holder.rs +++ b/pumpkin-world/src/chunk_system/chunk_holder.rs @@ -4,6 +4,11 @@ use slotmap::Key; pub struct ChunkHolder { pub target_stage: StagedChunkEnum, + /// Minimum stage required by generation dependencies (e.g. StructureReferences neighbors). + /// May exceed `target_stage` when this chunk is needed by a neighbor's generation task + /// but isn't in the player's view radius. Tasks up to this stage are scheduled but the + /// chunk is not made public and is unloaded once the dependency is satisfied. + pub dependency_stage: StagedChunkEnum, pub current_stage: StagedChunkEnum, pub chunk: Option, pub occupied: NodeKey, @@ -16,6 +21,7 @@ impl Default for ChunkHolder { fn default() -> Self { Self { target_stage: StagedChunkEnum::None, + dependency_stage: StagedChunkEnum::None, current_stage: StagedChunkEnum::None, chunk: None, occupied: NodeKey::null(), diff --git a/pumpkin-world/src/chunk_system/chunk_state.rs b/pumpkin-world/src/chunk_system/chunk_state.rs index 77592819d..d1d04ea76 100644 --- a/pumpkin-world/src/chunk_system/chunk_state.rs +++ b/pumpkin-world/src/chunk_system/chunk_state.rs @@ -115,7 +115,7 @@ impl StagedChunkEnum { match self { Self::Empty => 0, Self::StructureStart => 0, - Self::StructureReferences => 0, + Self::StructureReferences => 8, Self::Biomes => 0, Self::Noise => 0, Self::Surface => 0, @@ -131,7 +131,7 @@ impl StagedChunkEnum { match self { Self::Empty => 0, Self::StructureStart => 0, - Self::StructureReferences => 0, + Self::StructureReferences => 8, Self::Biomes => 0, Self::Noise => 0, Self::Surface => 0, @@ -148,7 +148,17 @@ impl StagedChunkEnum { // the Biome Step, this should be more efficient Self::Biomes => &[Self::Empty], Self::StructureStart => &[Self::Biomes], - Self::StructureReferences => &[Self::StructureStart], + Self::StructureReferences => &[ + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + Self::StructureStart, + ], Self::Noise => &[Self::StructureReferences], Self::Surface => &[Self::Noise], Self::Features => &[Self::Surface, Self::Surface], diff --git a/pumpkin-world/src/chunk_system/schedule.rs b/pumpkin-world/src/chunk_system/schedule.rs index 8a4816a65..7e8c057bd 100644 --- a/pumpkin-world/src/chunk_system/schedule.rs +++ b/pumpkin-world/src/chunk_system/schedule.rs @@ -59,6 +59,11 @@ pub struct GenerationSchedule { chunk_map: HashMap, unload_chunks: HashSetType, + /// Tasks that are graph-ready (in_degree == 0) but cannot yet run because + /// one or more of their required neighbor chunks haven't been delivered yet. + /// Parked here and re-queued by `check_waiting_tasks()` as chunk data arrives. + waiting_for_chunks: HashSetType, + io_lock: IOLock, running_task_count: u16, recv_chunk: crossfire::compat::MRx<(ChunkPos, RecvChunk)>, @@ -83,9 +88,6 @@ impl GenerationSchedule { let (send_read_io, recv_read_io) = crossfire::compat::mpmc::bounded_tx_blocking_rx_async(io_read_thread_count + 5); - // Use a bounded single-producer/single-consumer channel to prevent unbounded memory growth - // when IO can't keep up. If the queue reaches capacity, producers will block until - // the disk catches up. let (send_write_io, recv_write_io) = crossfire::compat::spsc::bounded_tx_blocking_rx_async(500); @@ -114,7 +116,7 @@ impl GenerationSchedule { let level_clone = level.clone(); let handle = thread::Builder::new() - .name(format!("Gen-{i}")) // Identifying dim helps debugging + .name(format!("Gen-{i}")) .spawn(move || { generation_work(recv_gen, send_chunk, level_clone); }) @@ -136,6 +138,7 @@ impl GenerationSchedule { send_level: level_channel, public_chunk_map: level_sched.loaded_chunks.clone(), unload_chunks: HashSetType::default(), + waiting_for_chunks: HashSetType::default(), io_lock, running_task_count: 0, recv_chunk, @@ -157,26 +160,22 @@ impl GenerationSchedule { match self.lighting_config { LightingEngineConfig::Full => { let mut engine = chunk.light_engine.lock().unwrap(); - for section in &mut engine.block_light { section.fill(15); } for section in &mut engine.sky_light { section.fill(15); } - chunk.dirty.store(true, Relaxed); } LightingEngineConfig::Dark => { let mut engine = chunk.light_engine.lock().unwrap(); - for section in &mut engine.block_light { section.fill(0); } for section in &mut engine.sky_light { section.fill(0); } - chunk.dirty.store(true, Relaxed); } _ => {} @@ -222,8 +221,13 @@ impl GenerationSchedule { self.queue = new_queue; } - // Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at - // `chunk_pos`) and wire it to depend on `dependency_task`. + /// Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at + /// `chunk_pos`) and wire it to depend on `dependency_task`. + /// + /// Bumps `holder.dependency_stage` (NOT `target_stage`) to at least `req_stage` so + /// that neighbor chunks pulled in as generation dependencies are not discarded before + /// their dependency is satisfied. `target_stage` is left alone so the level-change + /// bookkeeping invariant (`old_stage == holder.target_stage`) is never violated. #[allow(clippy::too_many_arguments)] pub(crate) fn ensure_dependency_chain( graph: &mut DAG, @@ -245,90 +249,157 @@ impl GenerationSchedule { graph.add_edge(holder.occupied, dependency_task); } + // Bump dependency_stage so this chunk's IO/generation tasks are scheduled and + // kept alive even if target_stage is None (outside player view radius). + // We deliberately do NOT touch target_stage — that field is owned by resort_work + // and must match the level-change bookkeeping or the debug_assert will fire. + if holder.dependency_stage < req_stage { + holder.dependency_stage = req_stage; + } + + // Effective target is the max of what the player wants and what dependencies need. + let effective_target = holder.target_stage.max(holder.dependency_stage); + + // Create any missing tasks from current_stage+1 up to effective_target. + // We do this even when current_stage >= req_stage, because dependency_stage may + // require tasks beyond req_stage that haven't been created yet. + if holder.current_stage < effective_target { + let empty = StagedChunkEnum::Empty as usize; + let start = (holder.current_stage as usize + 1).max(empty); + let end = effective_target as u8 as usize; + let mut newly_created = [false; StagedChunkEnum::COUNT]; + + for (i, flag) in newly_created[start..=end].iter_mut().enumerate() { + let stage_i = start + i; + if holder.tasks[stage_i].is_null() { + let new_node = graph + .nodes + .insert(Node::new(chunk_pos, StagedChunkEnum::from(stage_i as u8))); + holder.tasks[stage_i] = new_node; + *flag = true; + if !holder.occupied.is_null() { + graph.add_edge(holder.occupied, new_node); + } + } + } + + for stage_i in start..=end { + if !newly_created[stage_i] { + continue; + } + let cur = holder.tasks[stage_i]; + + if stage_i > empty { + let prev = holder.tasks[stage_i - 1]; + if !prev.is_null() { + graph.add_edge(prev, cur); + } + } + if stage_i < end { + let next = holder.tasks[stage_i + 1]; + if !next.is_null() && !newly_created[stage_i + 1] { + graph.add_edge(cur, next); + } + } + } + + // Queue the entry task (lowest unblocked stage) + let entry_task = holder.tasks[start]; + if !entry_task.is_null() + && let Some(n) = graph.nodes.get_mut(entry_task) + && n.in_degree == 0 + && !n.in_queue + { + n.in_queue = true; + queue.push(TaskHeapNode( + Self::calc_priority( + last_level, + last_high_priority, + chunk_pos, + StagedChunkEnum::from(start as u8), + ), + entry_task, + )); + } + } + + // If req_stage is already satisfied, dependency_task doesn't need to wait — + // it was only blocked on `occupied` (handled above) and the stage itself is done. + // Do NOT add an edge here: tasks[req_stage] is null (completed and dropped). if holder.current_stage >= req_stage { return; } - let empty = StagedChunkEnum::Empty as usize; - let start = (holder.current_stage as usize + 1).max(empty); - let end = req_stage as u8 as usize; - let mut newly_created = [false; StagedChunkEnum::COUNT]; - - for (i, flag) in newly_created[start..=end].iter_mut().enumerate() { - let stage_i = start + i; - if holder.tasks[stage_i].is_null() { - let new_node = graph - .nodes - .insert(Node::new(chunk_pos, StagedChunkEnum::from(stage_i as u8))); - holder.tasks[stage_i] = new_node; - *flag = true; - if !holder.occupied.is_null() { - graph.add_edge(holder.occupied, new_node); - } - } - } - - for stage_i in start..=end { - if !newly_created[stage_i] { - continue; - } - let cur = holder.tasks[stage_i]; - - if stage_i > empty { - let prev = holder.tasks[stage_i - 1]; - if !prev.is_null() { - graph.add_edge(prev, cur); - } - } - if stage_i < end { - let next = holder.tasks[stage_i + 1]; - if !next.is_null() && !newly_created[stage_i + 1] { - graph.add_edge(cur, next); - } - } - } - - // Queue the entry task (lowest unblocked stage) - let entry_task = holder.tasks[start]; - if !entry_task.is_null() - && let Some(n) = graph.nodes.get_mut(entry_task) - && n.in_degree == 0 - && !n.in_queue - { - n.in_queue = true; - queue.push(TaskHeapNode( - Self::calc_priority( - last_level, - last_high_priority, - chunk_pos, - StagedChunkEnum::from(start as u8), - ), - entry_task, - )); - } - - let ano_task = holder.tasks[end]; + // Wire req_stage task → dependency_task so dependency_task can't run until + // this chunk reaches req_stage. tasks[req_stage] is guaranteed non-null here: + // effective_target >= req_stage (we just set dependency_stage = req_stage) and + // current_stage < req_stage, so the task was created in the loop above (or + // already existed). + let req_end = req_stage as u8 as usize; + let ano_task = holder.tasks[req_end]; debug_assert!( !ano_task.is_null(), - "holder.tasks[end] must not be null before adding edge" + "holder.tasks[req_stage] must not be null before adding edge" ); graph.add_edge(ano_task, dependency_task); } + /// Check if any tasks parked in `waiting_for_chunks` now have all their neighbor + /// chunk data available, and re-queue them if so. + /// Must be called after every `receive_chunk` call. + fn check_waiting_tasks(&mut self) { + if self.waiting_for_chunks.is_empty() { + return; + } + + let mut now_ready: Vec = Vec::new(); + + self.waiting_for_chunks.retain(|&node_key| { + let Some(node) = self.graph.nodes.get(node_key) else { + return false; // node was dropped, discard silently + }; + let write_radius = node.stage.get_write_radius(); + let pos = node.pos; + let all_ready = (-write_radius..=write_radius).all(|dx| { + (-write_radius..=write_radius).all(|dy| { + self.chunk_map + .get(&pos.add_raw(dx, dy)) + .is_some_and(|h| h.chunk.is_some()) + }) + }); + if all_ready { + now_ready.push(node_key); + false + } else { + true + } + }); + + for node_key in now_ready { + if let Some(n) = self.graph.nodes.get_mut(node_key) + && n.in_degree == 0 + && !n.in_queue + { + n.in_queue = true; + let priority = + Self::calc_priority(&self.last_level, &self.last_high_priority, n.pos, n.stage); + self.queue.push(TaskHeapNode(priority, node_key)); + } + // If in_degree > 0, drop_node will re-queue when unblocked + } + } + fn resort_work(&mut self, new_data: (Option, Option>)) -> bool { - // true -> updated | false -> not update if new_data.0.is_none() && new_data.1.is_none() { return false; } if let Some(high_priority) = new_data.1 { - // debug!("receive new priority"); self.last_high_priority = high_priority; } let Some(new_level) = new_data.0 else { self.sort_queue(); return true; }; - // debug!("receive new level"); for (pos, (old_stage, new_stage)) in new_level.0 { debug_assert_ne!(old_stage, new_stage); debug_assert_eq!( @@ -340,15 +411,25 @@ impl GenerationSchedule { let mut holder = self.chunk_map.remove(&pos).unwrap_or_default(); debug_assert_eq!(holder.target_stage, old_stage); holder.target_stage = new_stage; - if old_stage > new_stage { - for i in (new_stage.max(holder.current_stage) as usize + 1)..=(old_stage as usize) { + + // Effective target is what we actually need to schedule tasks up to. + let effective_old = old_stage.max(holder.dependency_stage); + let effective_new = new_stage.max(holder.dependency_stage); + + if effective_old > effective_new { + for i in (effective_new.max(holder.current_stage) as usize + 1) + ..=(effective_old as usize) + { let task = &mut holder.tasks[i]; if !task.is_null() { - self.drop_node(*task); // Properly handles incoming/outgoing edges and in_degree + self.waiting_for_chunks.remove(task); + self.drop_node(*task); *task = NodeKey::null(); } } - if new_stage == StagedChunkEnum::None { + if new_stage == StagedChunkEnum::None + && holder.dependency_stage == StagedChunkEnum::None + { self.unload_chunks.insert(pos); } } else { @@ -366,7 +447,8 @@ impl GenerationSchedule { } } } - for i in (old_stage.max(holder.current_stage) as u8 + 1)..=(new_stage as u8) { + for i in (effective_old.max(holder.current_stage) as u8 + 1)..=(effective_new as u8) + { let task = &mut holder.tasks[i as usize]; if task.is_null() { *task = self.graph.nodes.insert(Node::new(pos, i.into())); @@ -412,7 +494,7 @@ impl GenerationSchedule { } } let node = self.graph.nodes.get_mut(task).unwrap(); - if node.in_degree == 0 { + if node.in_degree == 0 && !node.in_queue { node.in_queue = true; self.queue.push(TaskHeapNode(0, task)); } @@ -446,7 +528,6 @@ impl GenerationSchedule { } let sc = Arc::strong_count(&chunk); if sc == 1 { - // debug!("unload chunk {pos:?} to file"); chunks.push((pos, Chunk::Level(chunk))); self.chunk_map.remove(&pos); } else { @@ -460,14 +541,12 @@ impl GenerationSchedule { } Chunk::Proto(chunk) => { debug_assert!(!holder.public); - // debug!("unload proto chunk {pos:?} to file"); chunks.push((pos, Chunk::Proto(chunk))); self.chunk_map.remove(&pos); } } } } - // debug!("send {} unloaded chunks to io write", chunks.len()); if chunks.is_empty() { return; } @@ -490,7 +569,6 @@ impl GenerationSchedule { if let Some(chunk) = &holder.chunk { match chunk { Chunk::Level(sync_chunk) => { - // Only save level chunks that are marked dirty if sync_chunk.is_dirty() { chunks.push((*pos, Chunk::Level(sync_chunk.clone()))); } @@ -528,7 +606,6 @@ impl GenerationSchedule { let Some(old) = self.graph.nodes.remove(node) else { return; }; - // debug!("drop node {node:?}"); let mut edge = old.edge; while !edge.is_null() { let cur = self.graph.edges.remove(edge).unwrap(); @@ -536,16 +613,20 @@ impl GenerationSchedule { debug_assert!(node.in_degree >= 1); node.in_degree -= 1; if node.in_degree == 0 && !node.in_queue { - self.queue.push(TaskHeapNode( - Self::calc_priority( - &self.last_level, - &self.last_high_priority, - node.pos, - node.stage, - ), - cur.to, - )); - node.in_queue = true; + // Don't queue if parked in waiting_for_chunks — check_waiting_tasks() + // will re-queue it once chunk data arrives. + if !self.waiting_for_chunks.contains(&cur.to) { + self.queue.push(TaskHeapNode( + Self::calc_priority( + &self.last_level, + &self.last_high_priority, + node.pos, + node.stage, + ), + cur.to, + )); + node.in_queue = true; + } } } edge = cur.next; @@ -555,7 +636,6 @@ impl GenerationSchedule { fn receive_chunk(&mut self, pos: ChunkPos, data: RecvChunk) { match data { RecvChunk::IO(chunk) => { - // debug!("receive io chunk pos {pos:?}"); let mut holder = self.chunk_map.remove(&pos).unwrap(); if holder.chunk.is_some() { warn!( @@ -576,7 +656,6 @@ impl GenerationSchedule { match &chunk { Chunk::Level(data) => { - // Full chunk from IO - mark as public and notify self.apply_lighting_override(data); let result = self.public_chunk_map.insert(pos, data.clone()); if result.is_some() { @@ -593,8 +672,6 @@ impl GenerationSchedule { self.listener.process_new_chunk(pos, data); } Chunk::Proto(_) => { - // Proto chunk from IO (downgraded for relighting) - mark as non-public - // so players get notified when it finishes generation if holder.public { debug!( "Chunk {:?} downgraded to Proto for relighting, marking as non-public", @@ -607,9 +684,11 @@ impl GenerationSchedule { } holder.chunk = Some(chunk); self.chunk_map.insert(pos, holder); + + // A new chunk arrived — unblock any waiting generation tasks + self.check_waiting_tasks(); } RecvChunk::Generation(data) => { - // debug!("receive gen chunk pos {pos:?}"); let mut dx = 0; let mut dy = 0; for chunk in data.chunks { @@ -618,8 +697,6 @@ impl GenerationSchedule { Chunk::Level(chunk) => { let mut holder = self.chunk_map.remove(&new_pos).unwrap(); if new_pos == pos { - // Expect the holder to be one stage before Full (Lighting). If not, - // log and align so we don't panic in production/debug runs. if holder.current_stage != StagedChunkEnum::Lighting { warn!( "receive_chunk(Level): holder at {:?} for pos {:?} expected {:?}; aligning", @@ -636,12 +713,9 @@ impl GenerationSchedule { } holder.current_stage = StagedChunkEnum::Full; - // Check if this is the first time becoming public let was_public = holder.public; if was_public { - // Was already public but chunk has been regenerated (e.g., after relighting) - // Update the chunk data and re-notify players self.apply_lighting_override(&chunk); holder.chunk = Some(Chunk::Level(chunk.clone())); self.public_chunk_map.insert(new_pos, chunk.clone()); @@ -652,7 +726,6 @@ impl GenerationSchedule { self.listener.process_new_chunk(new_pos, &chunk); } else { self.apply_lighting_override(&chunk); - // Clone once for public_chunk_map (needed for player access) let public_chunk = chunk.clone(); holder.chunk = Some(Chunk::Level(chunk)); let result = @@ -663,7 +736,6 @@ impl GenerationSchedule { "public_chunk_map.insert returned existing chunk for {new_pos:?}" ); } - // Notify players about the new chunk if let Some(pc) = self.public_chunk_map.get(&new_pos) { trace!( "Notifying players: new chunk at {:?} (generation complete)", @@ -678,23 +750,31 @@ impl GenerationSchedule { } } } else { - // Non-center chunk... just restore it to holder without cloning holder.chunk = Some(Chunk::Level(chunk)); } - // Always drop the occupied node before nulling it if !holder.occupied.is_null() && self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } holder.occupied = NodeKey::null(); + + // If this neighbor chunk was only loaded for a dependency and + // is no longer needed, clear dependency_stage and queue unload. + if holder.target_stage == StagedChunkEnum::None + && new_pos != pos + && holder.current_stage >= holder.dependency_stage + { + holder.dependency_stage = StagedChunkEnum::None; + self.unload_chunks.insert(new_pos); + } + self.chunk_map.insert(new_pos, holder); } Chunk::Proto(chunk) => { let mut holder = self.chunk_map.remove(&new_pos).unwrap(); - // Clean up the task node for the stage that just finished. let stage = chunk.stage_id(); if stage < holder.tasks.len() as u8 { let task_idx = stage as usize; @@ -705,23 +785,28 @@ impl GenerationSchedule { } if new_pos == pos { - // Center Chunk Logic debug_assert_ne!(holder.current_stage, StagedChunkEnum::None); if self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } holder.current_stage = StagedChunkEnum::from(stage); } else { - // Neighbor Logic if holder.current_stage < StagedChunkEnum::from(stage) { holder.current_stage = StagedChunkEnum::from(stage); } - if !holder.occupied.is_null() && self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } + + // Clear dependency_stage and queue unload if no longer needed + if holder.target_stage == StagedChunkEnum::None + && holder.current_stage >= holder.dependency_stage + { + holder.dependency_stage = StagedChunkEnum::None; + self.unload_chunks.insert(new_pos); + } } holder.occupied = NodeKey::null(); @@ -735,6 +820,9 @@ impl GenerationSchedule { dx += 1; } } + + // Neighbor chunks returned to holders — unblock waiting tasks + self.check_waiting_tasks(); } RecvChunk::GenerationFailure { pos: fail_pos, @@ -746,11 +834,9 @@ impl GenerationSchedule { fail_pos, stage, error ); - // Clean up the holder if let Some(mut holder) = self.chunk_map.remove(&pos) { let target_stage = holder.target_stage; - // Clean up occupied node if !holder.occupied.is_null() { if self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); @@ -758,31 +844,28 @@ impl GenerationSchedule { holder.occupied = NodeKey::null(); } - // Clean up all task nodes for i in 0..holder.tasks.len() { if !holder.tasks[i].is_null() { + self.waiting_for_chunks.remove(&holder.tasks[i]); self.drop_node(holder.tasks[i]); holder.tasks[i] = NodeKey::null(); } } - // Reset chunk to None so it will be regenerated holder.current_stage = StagedChunkEnum::None; + holder.dependency_stage = StagedChunkEnum::None; holder.chunk = None; - // Recreate tasks from None to target_stage for i in (StagedChunkEnum::None as usize + 1)..=(target_stage as usize) { let stage_enum = StagedChunkEnum::from(i as u8); let task_node = Node::new(pos, stage_enum); holder.tasks[i] = self.graph.nodes.insert(task_node); - // Add dependencies if i > (StagedChunkEnum::None as usize + 1) { self.graph.add_edge(holder.tasks[i - 1], holder.tasks[i]); } } - // Add first task to queue with high priority (use a boost to retry faster) if target_stage > StagedChunkEnum::None { let first_task = holder.tasks[StagedChunkEnum::None as usize + 1]; if let Some(node) = self.graph.nodes.get_mut(first_task) { @@ -794,7 +877,7 @@ impl GenerationSchedule { &self.last_high_priority, pos, StagedChunkEnum::from(1), - ) - 50, // Priority boost for retry + ) - 50, first_task, )); } @@ -819,7 +902,6 @@ impl GenerationSchedule { thread::current().id(), thread::current().name().unwrap_or("unknown") ); - // let mut clock = Instant::now(); loop { if level.should_unload.swap(false, Relaxed) { self.unload_chunk(); @@ -828,16 +910,13 @@ impl GenerationSchedule { self.save_all_chunk(false); } if level.shut_down_chunk_system.load(Relaxed) { - // Save all chunks BEFORE breaking the loop to ensure IO write thread processes them info!("Saving chunks before shutdown..."); self.save_all_chunk(true); break; } 'out2: while let Some(task) = self.queue.pop() { - // Check shutdown flag again before processing tasks to avoid IO errors if level.shut_down_chunk_system.load(Relaxed) { - // Don't process any more tasks, just break to save chunks self.queue.push(task); info!("Shutdown detected during task processing, saving chunks..."); self.save_all_chunk(true); @@ -866,31 +945,60 @@ impl GenerationSchedule { ChunkPos::new(i32::MAX, i32::MAX), StagedChunkEnum::None, )); - for i in - (holder.current_stage as usize + 1)..=(holder.target_stage as usize) - { + let effective_target = holder.target_stage.max(holder.dependency_stage); + for i in (holder.current_stage as usize + 1)..=(effective_target as usize) { self.graph.add_edge(occupy, holder.tasks[i]); } holder.occupied = occupy; - // debug!("send task {:?} {node:?}", task.1); - if self.io_read.send(node.pos).is_err() { - // IO thread closed (likely due to shutdown), save and exit cleanly info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); break 'out2; } } else { let write_radius = node.stage.get_write_radius(); + + // Pre-validate that every chunk in the write area (including the + // center for write_radius==0 stages like Biomes, StructureStart, + // Noise, Surface) has its data present before we swap anything out. + // + // The dependency graph ensures predecessor *tasks* are complete, but + // there is a brief window between a task completing on a generation + // thread and its chunk data being placed back into the holder. Any + // stage whose write area overlaps with a currently-running task will + // see chunk==None in that window. We park here and let + // check_waiting_tasks() re-queue once all data has arrived. + { + let all_ready = (-write_radius..=write_radius).all(|dx| { + (-write_radius..=write_radius).all(|dy| { + self.chunk_map + .get(&node.pos.add_raw(dx, dy)) + .is_some_and(|h| h.chunk.is_some()) + }) + }); + + if !all_ready { + if let Some(n) = self.graph.nodes.get_mut(task.1) { + n.in_queue = false; + } + self.waiting_for_chunks.insert(task.1); + // Close the TOCTOU window: the chunk we're waiting for may + // have arrived in the recv_chunk drain that happened earlier + // in this same loop iteration, before this task was parked. + // If so, check_waiting_tasks() will immediately re-queue it + // so it isn't stranded with running_task_count==0. + self.check_waiting_tasks(); + continue; + } + } + let mut cache = Cache::new( node.pos.x - write_radius, node.pos.y - write_radius, write_radius << 1 | 1, ); - // Only create the occupy node after we know all chunks are available - // and we're actually proceeding with the task let occupy = self.graph.nodes.insert(Node::new( ChunkPos::new(i32::MAX, i32::MAX), StagedChunkEnum::None, @@ -911,8 +1019,6 @@ impl GenerationSchedule { }; match tmp { Chunk::Level(chunk) => { - // Don't clone Level chunks. Move them into cache to avoid - // extra Arc references that prevent unloading. cache.chunks.push(Chunk::Level(chunk)); } Chunk::Proto(chunk) => { @@ -951,11 +1057,8 @@ impl GenerationSchedule { } } - // debug!("send task {:?} {node:?}", task.1); - self.running_task_count += 1; if self.generate.send((node.pos, cache, node.stage)).is_err() { - // revert running task count increment and exit cleanly with save self.running_task_count = self.running_task_count.saturating_sub(1); info!("Generation thread closed, saving remaining chunks..."); self.save_all_chunk(true); @@ -966,8 +1069,10 @@ impl GenerationSchedule { } if self.queue.is_empty() { - // debug!("the queue is empty. thread sleep"); - while self.running_task_count > 0 && self.queue.is_empty() { + // Wait while there are in-flight tasks OR tasks parked waiting for chunk data. + while (self.running_task_count > 0 || !self.waiting_for_chunks.is_empty()) + && self.queue.is_empty() + { if let Ok((pos, data)) = self.recv_chunk.try_recv() { self.receive_chunk(pos, data); self.resort_work(self.send_level.get()); @@ -978,8 +1083,7 @@ impl GenerationSchedule { thread::sleep(Duration::from_millis(50)); } } - if self.queue.is_empty() { - // debug!("no work to do. thread sleep"); + if self.queue.is_empty() && self.waiting_for_chunks.is_empty() { debug_assert!(self.debug_check()); debug_assert_eq!(self.running_task_count, 0); self.resort_work(self.send_level.wait_and_get(&level)); @@ -995,7 +1099,7 @@ impl GenerationSchedule { while self.running_task_count > 0 && wait_iterations < max_wait_iterations { if let Ok((pos, data)) = self.recv_chunk.try_recv() { self.receive_chunk(pos, data); - wait_iterations = 0; // Reset counter when we receive data + wait_iterations = 0; } else { wait_iterations += 1; if wait_iterations % 20 == 0 { @@ -1014,14 +1118,12 @@ impl GenerationSchedule { "Cancelling {} in-flight generation tasks", self.running_task_count ); - // Collect nodes to drop let mut nodes_to_drop = Vec::new(); - // Clear occupy nodes for cancelled tasks for holder in self.chunk_map.values_mut() { - // Collect any task nodes associated with this holder to ensure full cleanup for task in &mut holder.tasks { if !task.is_null() { + self.waiting_for_chunks.remove(task); nodes_to_drop.push(*task); *task = NodeKey::null(); } @@ -1032,13 +1134,11 @@ impl GenerationSchedule { && node.pos.x == i32::MAX && node.pos.y == i32::MAX { - // This is an occupy node for an in-flight task nodes_to_drop.push(holder.occupied); holder.occupied = NodeKey::null(); } } - // Safely drop them (using `drop_node`, which decrements in_degree on targets) for node_key in nodes_to_drop { self.drop_node(node_key); } @@ -1046,11 +1146,8 @@ impl GenerationSchedule { self.running_task_count = 0; } - // Drop the io_write sender to signal the write thread to exit - // Chunks were already saved during loop exit above drop(self.io_write); - // Clean up any remaining graph structures let unreleased_count = self.graph.nodes.len(); if unreleased_count > 0 { warn!( @@ -1063,7 +1160,6 @@ impl GenerationSchedule { fn debug_check(&self) -> bool { if !self.graph.nodes.is_empty() { - // error!("nodes: {:?}", self.graph.nodes); for (key, value) in &self.graph.nodes { error!("unrelease node {key:?}: {value:?}"); } @@ -1079,7 +1175,8 @@ impl GenerationSchedule { *self.last_level.get(pos).unwrap_or(&ChunkLoading::MAX_LEVEL) ) ); - debug_assert!(holder.current_stage >= holder.target_stage); + let effective = holder.target_stage.max(holder.dependency_stage); + debug_assert!(holder.current_stage >= effective); debug_assert!(holder.occupied.is_null()); if holder.current_stage != StagedChunkEnum::None { debug_assert_eq!( diff --git a/pumpkin-world/src/generation/generator/mod.rs b/pumpkin-world/src/generation/generator/mod.rs index ce61ff1f8..d5b926fb3 100644 --- a/pumpkin-world/src/generation/generator/mod.rs +++ b/pumpkin-world/src/generation/generator/mod.rs @@ -26,7 +26,7 @@ pub struct VanillaGenerator { impl GeneratorInit for VanillaGenerator { fn new(seed: Seed, dimension: Dimension) -> Self { - let random_config = GlobalRandomConfig::new(seed.0, false); + let random_config = GlobalRandomConfig::new(seed.0); // TODO: The generation settings contains (part of?) the noise routers too; do we keep the separate or // use only the generation settings? diff --git a/pumpkin-world/src/generation/mod.rs b/pumpkin-world/src/generation/mod.rs index 161235cc8..6508d6b40 100644 --- a/pumpkin-world/src/generation/mod.rs +++ b/pumpkin-world/src/generation/mod.rs @@ -21,10 +21,7 @@ mod surface; use generator::{GeneratorInit, VanillaGenerator}; use pumpkin_data::dimension::Dimension; use pumpkin_util::{ - random::{ - RandomDeriver, RandomDeriverImpl, RandomImpl, legacy_rand::LegacyRand, - xoroshiro128::Xoroshiro, - }, + random::xoroshiro128::{Xoroshiro, XoroshiroSplitter}, world_seed::Seed, }; @@ -36,19 +33,15 @@ pub fn get_world_gen(seed: Seed, dimension: Dimension) -> Box pub struct GlobalRandomConfig { pub seed: u64, - base_random_deriver: RandomDeriver, - aquifer_random_deriver: RandomDeriver, - ore_random_deriver: RandomDeriver, + base_random_deriver: XoroshiroSplitter, + aquifer_random_deriver: XoroshiroSplitter, + pub ore_random_deriver: XoroshiroSplitter, } impl GlobalRandomConfig { #[must_use] - pub fn new(seed: u64, legacy: bool) -> Self { - let random_deriver = if legacy { - LegacyRand::from_seed(seed).next_splitter() - } else { - Xoroshiro::from_seed(seed).next_splitter() - }; + pub fn new(seed: u64) -> Self { + let random_deriver = Xoroshiro::from_seed(seed).next_splitter(); let aquifer_deriver = random_deriver .split_string("minecraft:aquifer") diff --git a/pumpkin-world/src/generation/noise/aquifer_sampler.rs b/pumpkin-world/src/generation/noise/aquifer_sampler.rs index 64a218391..e89466f32 100644 --- a/pumpkin-world/src/generation/noise/aquifer_sampler.rs +++ b/pumpkin-world/src/generation/noise/aquifer_sampler.rs @@ -2,7 +2,7 @@ use enum_dispatch::enum_dispatch; use pumpkin_data::{Block, BlockState}; use pumpkin_util::{ math::{clamped_map, floor_div, vector3::Vector3}, - random::{RandomDeriver, RandomDeriverImpl, RandomImpl}, + random::{RandomImpl, xoroshiro128::XoroshiroSplitter}, }; use crate::generation::{ @@ -139,7 +139,7 @@ impl WorldAquiferSampler { pub fn new( chunk_x: i32, chunk_z: i32, - random_deriver: &RandomDeriver, + random_deriver: &XoroshiroSplitter, minimum_y: i8, height: u16, fluid_level: FluidLevelSampler, @@ -725,7 +725,7 @@ mod random_positions_and_hypot { const SEED: u64 = 0; static RANDOM_CONFIG: LazyLock = - LazyLock::new(|| GlobalRandomConfig::new(SEED, false)); + LazyLock::new(|| GlobalRandomConfig::new(SEED)); static PROTO_ROUTER: LazyLock = LazyLock::new(|| { let router_ast = &OVERWORLD_BASE_NOISE_ROUTER; ProtoNoiseRouters::generate(router_ast, &RANDOM_CONFIG) diff --git a/pumpkin-world/src/generation/noise/mod.rs b/pumpkin-world/src/generation/noise/mod.rs index 456f0c99c..bcd6c6429 100644 --- a/pumpkin-world/src/generation/noise/mod.rs +++ b/pumpkin-world/src/generation/noise/mod.rs @@ -4,7 +4,10 @@ pub mod perlin; pub mod router; use pumpkin_data::{Block, BlockState, chunk_gen_settings::GenerationShapeConfig}; -use pumpkin_util::math::{floor_div, floor_mod, vector3::Vector3}; +use pumpkin_util::{ + math::{floor_div, floor_mod, vector3::Vector3}, + random::xoroshiro128::XoroshiroSplitter, +}; use crate::generation::{ noise::{ @@ -45,14 +48,14 @@ impl BlockStateSampler { pub fn sample( &mut self, router: &mut ChunkNoiseRouter, - random_config: &GlobalRandomConfig, + ore_random_deriver: &XoroshiroSplitter, pos: &Vector3, sample_options: &ChunkNoiseFunctionSampleOptions, height_estimator: &mut SurfaceHeightEstimateSampler, ) -> Option<&'static BlockState> { match self { Self::Aquifer(aquifer) => aquifer.apply(router, pos, sample_options, height_estimator), - Self::Ore(ore) => ore.sample(router, random_config, pos, sample_options), + Self::Ore(ore) => ore.sample(router, ore_random_deriver, pos, sample_options), } } } @@ -70,15 +73,19 @@ impl ChainedBlockStateSampler { fn sample( &mut self, router: &mut ChunkNoiseRouter, - random_config: &GlobalRandomConfig, + ore_random_deriver: &XoroshiroSplitter, pos: &Vector3, sample_options: &ChunkNoiseFunctionSampleOptions, height_estimator: &mut SurfaceHeightEstimateSampler, ) -> Option<&'static BlockState> { for sampler in &mut self.samplers { - if let Some(state) = - sampler.sample(router, random_config, pos, sample_options, height_estimator) - { + if let Some(state) = sampler.sample( + router, + ore_random_deriver, + pos, + sample_options, + height_estimator, + ) { return Some(state); } } @@ -371,7 +378,7 @@ impl<'a> ChunkNoiseGenerator<'a> { #[expect(clippy::too_many_arguments)] pub fn sample_block_state( &mut self, - random_config: &GlobalRandomConfig, + ore_random_deriver: &XoroshiroSplitter, start_x: i32, start_y: i32, start_z: i32, @@ -399,7 +406,7 @@ impl<'a> ChunkNoiseGenerator<'a> { self.state_sampler.sample( &mut self.router, - random_config, + ore_random_deriver, &pos, &options, height_estimator, diff --git a/pumpkin-world/src/generation/noise/ore_sampler.rs b/pumpkin-world/src/generation/noise/ore_sampler.rs index 741f21e00..09804f643 100644 --- a/pumpkin-world/src/generation/noise/ore_sampler.rs +++ b/pumpkin-world/src/generation/noise/ore_sampler.rs @@ -1,15 +1,11 @@ use pumpkin_data::{Block, BlockState}; use pumpkin_util::{ math::{clamped_map, vector3::Vector3}, - random::{RandomDeriverImpl, RandomImpl}, + random::{RandomImpl, xoroshiro128::XoroshiroSplitter}, }; -use crate::{ - GlobalRandomConfig, - generation::noise::router::{ - chunk_density_function::ChunkNoiseFunctionSampleOptions, - chunk_noise_router::ChunkNoiseRouter, - }, +use crate::generation::noise::router::{ + chunk_density_function::ChunkNoiseFunctionSampleOptions, chunk_noise_router::ChunkNoiseRouter, }; pub struct OreVeinSampler; @@ -18,7 +14,7 @@ impl OreVeinSampler { pub fn sample( &self, router: &mut ChunkNoiseRouter, - random_config: &GlobalRandomConfig, + ore_random_deriver: &XoroshiroSplitter, pos: &Vector3, sample_options: &ChunkNoiseFunctionSampleOptions, ) -> Option<&'static BlockState> { @@ -37,9 +33,7 @@ impl OreVeinSampler { let mapped_diff = clamped_map(closest_to_bound as f64, 0f64, 20f64, -0.2f64, 0f64); let abs_sample = vein_toggle.abs(); if abs_sample + mapped_diff >= 0.4f32 as f64 { - let mut random = random_config - .ore_random_deriver - .split_pos(pos.x, block_y, pos.z); + let mut random = ore_random_deriver.split_pos(pos.x, block_y, pos.z); let vein_ridged_sample = router.vein_ridged(pos, sample_options); if random.next_f32() <= 0.7f32 && vein_ridged_sample < 0f64 { diff --git a/pumpkin-world/src/generation/noise/perlin.rs b/pumpkin-world/src/generation/noise/perlin.rs index 22dbed3cb..75f17cb2e 100644 --- a/pumpkin-world/src/generation/noise/perlin.rs +++ b/pumpkin-world/src/generation/noise/perlin.rs @@ -1,5 +1,5 @@ use pumpkin_data::chunk::DoublePerlinNoiseParameters; -use pumpkin_util::{noise::perlin::OctavePerlinNoiseSampler, random::RandomGenerator}; +use pumpkin_util::{noise::perlin::OctavePerlinNoiseSampler, random::RandomImpl}; pub struct DoublePerlinNoiseSampler { first_sampler: OctavePerlinNoiseSampler, @@ -9,7 +9,7 @@ pub struct DoublePerlinNoiseSampler { } impl DoublePerlinNoiseSampler { - fn create_amplitude(octaves: i32) -> f64 { + const fn create_amplitude(octaves: i32) -> f64 { 0.1f64 * (1f64 + 1f64 / (octaves + 1) as f64) } @@ -19,7 +19,7 @@ impl DoublePerlinNoiseSampler { } pub fn from_params( - rand: &mut RandomGenerator, + rand: &mut impl RandomImpl, parameters: &DoublePerlinNoiseParameters, legacy: bool, ) -> Self { @@ -27,7 +27,7 @@ impl DoublePerlinNoiseSampler { } pub fn new( - rand: &mut RandomGenerator, + rand: &mut impl RandomImpl, first_octave: i32, amplitudes: &[f64], legacy: bool, diff --git a/pumpkin-world/src/generation/noise/router/chunk_density_function.rs b/pumpkin-world/src/generation/noise/router/chunk_density_function.rs index 223504bef..63ed58d54 100644 --- a/pumpkin-world/src/generation/noise/router/chunk_density_function.rs +++ b/pumpkin-world/src/generation/noise/router/chunk_density_function.rs @@ -58,7 +58,7 @@ pub struct WrapperData { impl WrapperData { #[must_use] - pub fn new( + pub const fn new( cell_x_block_position: usize, cell_y_block_position: usize, cell_z_block_position: usize, @@ -77,7 +77,7 @@ impl WrapperData { } } - pub fn update_position( + pub const fn update_position( &mut self, cell_x_block_position: usize, cell_y_block_position: usize, @@ -246,7 +246,7 @@ impl DensityInterpolator { cell_z_position * (self.vertical_cell_count + 1) + cell_y_position } - pub(crate) fn on_sampled_cell_corners( + pub(crate) const fn on_sampled_cell_corners( &mut self, cell_y_position: usize, cell_z_position: usize, diff --git a/pumpkin-world/src/generation/noise/router/density_function/noise.rs b/pumpkin-world/src/generation/noise/router/density_function/noise.rs index 13d04fc73..08a3ae658 100644 --- a/pumpkin-world/src/generation/noise/router/density_function/noise.rs +++ b/pumpkin-world/src/generation/noise/router/density_function/noise.rs @@ -4,7 +4,7 @@ use pumpkin_data::noise_router::{InterpolatedNoiseSamplerData, NoiseData, Shifte use pumpkin_util::{ math::{clamped_lerp, vector3::Vector3}, noise::perlin::OctavePerlinNoiseSampler, - random::RandomGenerator, + random::xoroshiro128::Xoroshiro, }; use crate::generation::{ @@ -190,7 +190,7 @@ pub struct InterpolatedNoiseSampler { } impl InterpolatedNoiseSampler { - pub fn new(data: &'static InterpolatedNoiseSamplerData, random: &mut RandomGenerator) -> Self { + pub fn new(data: &'static InterpolatedNoiseSamplerData, random: &mut Xoroshiro) -> Self { let big_start = -15; let big_amplitudes = [1.0; 16]; diff --git a/pumpkin-world/src/generation/noise/router/density_function/test.rs b/pumpkin-world/src/generation/noise/router/density_function/test.rs index 88fbcce9d..a1fc16f45 100644 --- a/pumpkin-world/src/generation/noise/router/density_function/test.rs +++ b/pumpkin-world/src/generation/noise/router/density_function/test.rs @@ -26,7 +26,7 @@ static TEST_OPTIONS: ChunkNoiseFunctionBuilderOptions = ChunkNoiseFunctionBuilderOptions::new(0, 0, 0, 0, 0, 0, 0); const SEED: u64 = 0; static RANDOM_CONFIG: LazyLock = - LazyLock::new(|| GlobalRandomConfig::new(SEED, false)); + LazyLock::new(|| GlobalRandomConfig::new(SEED)); macro_rules! build_function_stack { ($stack:expr) => {{ diff --git a/pumpkin-world/src/generation/noise/router/multi_noise_sampler.rs b/pumpkin-world/src/generation/noise/router/multi_noise_sampler.rs index 80f28ca1e..83ad2a0c5 100644 --- a/pumpkin-world/src/generation/noise/router/multi_noise_sampler.rs +++ b/pumpkin-world/src/generation/noise/router/multi_noise_sampler.rs @@ -238,7 +238,7 @@ mod test { #[test] fn sample() { let seed = 123; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let noise_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); let multi_noise_config = MultiNoiseSamplerBuilderOptions::new(1, 1, 1); @@ -259,7 +259,7 @@ mod test { fn sample_2() { // we use a different seed let seed = 13579; - let random_config = GlobalRandomConfig::new(seed, false); + let random_config = GlobalRandomConfig::new(seed); let noise_router = ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config); let multi_noise_config = MultiNoiseSamplerBuilderOptions::new(1, 1, 1); diff --git a/pumpkin-world/src/generation/noise/router/proto_noise_router.rs b/pumpkin-world/src/generation/noise/router/proto_noise_router.rs index 5b1cdac88..bc8fed7db 100644 --- a/pumpkin-world/src/generation/noise/router/proto_noise_router.rs +++ b/pumpkin-world/src/generation/noise/router/proto_noise_router.rs @@ -6,7 +6,7 @@ use pumpkin_data::{ UnaryOperation, }, }; -use pumpkin_util::random::RandomDeriverImpl; +use pumpkin_util::random::xoroshiro128::XoroshiroSplitter; use crate::{GlobalRandomConfig, generation::noise::perlin::DoublePerlinNoiseSampler}; @@ -59,14 +59,14 @@ pub enum ProtoNoiseFunctionComponent { } pub struct DoublePerlinNoiseBuilder<'a> { - random_config: &'a GlobalRandomConfig, + base_random_deriver: &'a XoroshiroSplitter, } impl<'a> DoublePerlinNoiseBuilder<'a> { #[must_use] - pub const fn new(rand: &'a GlobalRandomConfig) -> Self { + pub const fn new(base_random_deriver: &'a XoroshiroSplitter) -> Self { Self { - random_config: rand, + base_random_deriver, } } @@ -76,10 +76,7 @@ impl<'a> DoublePerlinNoiseBuilder<'a> { .unwrap_or_else(|| panic!("Unknown noise id: {id}")); // Note that the parameters' id is different than `id` - let mut random = self - .random_config - .base_random_deriver - .split_string(parameters.id()); + let mut random = self.base_random_deriver.split_string(parameters.id()); DoublePerlinNoiseSampler::from_params(&mut random, parameters, false) } } @@ -144,7 +141,8 @@ impl ProtoNoiseRouters { base_stack: &[BaseNoiseFunctionComponent], random_config: &GlobalRandomConfig, ) -> Box<[ProtoNoiseFunctionComponent]> { - let perlin_noise_builder = DoublePerlinNoiseBuilder::new(random_config); + let perlin_noise_builder = + DoublePerlinNoiseBuilder::new(&random_config.base_random_deriver); // Contiguous memory for our function components let mut stack = Vec::::with_capacity(base_stack.len()); diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 5272645ce..ca277630b 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -10,6 +10,7 @@ use pumpkin_data::structures::{ }; use pumpkin_data::tag; use pumpkin_data::{Block, BlockState, block_properties::blocks_movement, chunk::Biome}; +use pumpkin_util::random::xoroshiro128::XoroshiroSplitter; use pumpkin_util::random::{RandomImpl, get_carver_seed}; use pumpkin_util::{ HeightMap, @@ -171,7 +172,7 @@ impl TerrainCache { #[must_use] pub fn from_random(random_config: &GlobalRandomConfig) -> Self { let random = &random_config.base_random_deriver; - let noise_builder = DoublePerlinNoiseBuilder::new(random_config); + let noise_builder = DoublePerlinNoiseBuilder::new(&random_config.base_random_deriver); let terrain_builder = SurfaceTerrainBuilder::new(&noise_builder, random); let surface_noise = noise_builder.get_noise_sampler_for_id("surface"); let secondary_noise = noise_builder.get_noise_sampler_for_id("surface_secondary"); @@ -595,7 +596,7 @@ impl ProtoChunk { ); self.populate_noise( &mut noise_sampler, - random_config, + &random_config.ore_random_deriver, &mut surface_height_estimate_sampler, ); @@ -698,7 +699,7 @@ impl ProtoChunk { pub fn populate_noise( &mut self, noise_sampler: &mut ChunkNoiseGenerator, - random_config: &GlobalRandomConfig, + ore_random_deriver: &XoroshiroSplitter, surface_height_estimate_sampler: &mut SurfaceHeightEstimateSampler, ) { let h_count = noise_sampler.horizontal_cell_block_count() as i32; @@ -750,7 +751,7 @@ impl ProtoChunk { let block_state = noise_sampler .sample_block_state( - random_config, + ore_random_deriver, sample_start_x, sample_start_y, sample_start_z, @@ -805,7 +806,7 @@ impl ProtoChunk { let min_y = self.bottom_y(); let random = &random_config.base_random_deriver; - let noise_builder = DoublePerlinNoiseBuilder::new(random_config); + let noise_builder = DoublePerlinNoiseBuilder::new(&random_config.base_random_deriver); let mut context = MaterialRuleContext::new( min_y, self.height(), diff --git a/pumpkin-world/src/generation/surface/mod.rs b/pumpkin-world/src/generation/surface/mod.rs index 236711205..2ce32924f 100644 --- a/pumpkin-world/src/generation/surface/mod.rs +++ b/pumpkin-world/src/generation/surface/mod.rs @@ -8,7 +8,7 @@ use pumpkin_data::{ }; use pumpkin_util::{ math::{lerp2, vertical_surface_type::VerticalSurfaceType}, - random::{RandomDeriver, RandomDeriverImpl, RandomImpl}, + random::{RandomImpl, xoroshiro128::XoroshiroSplitter}, }; use terrain::SurfaceTerrainBuilder; @@ -32,7 +32,7 @@ pub mod terrain; pub struct MaterialRuleContext<'a> { pub min_y: i8, pub height: u16, - pub random_deriver: &'a RandomDeriver, + pub random_deriver: &'a XoroshiroSplitter, fluid_height: i32, pub block_pos_x: i32, pub block_pos_y: i32, @@ -61,7 +61,7 @@ impl<'a> MaterialRuleContext<'a> { min_y: i8, height: u16, noise_builder: DoublePerlinNoiseBuilder<'a>, - random_deriver: &'a RandomDeriver, + random_deriver: &'a XoroshiroSplitter, terrain_builder: &'a SurfaceTerrainBuilder, surface_noise: &'a DoublePerlinNoiseSampler, secondary_noise: &'a DoublePerlinNoiseSampler, diff --git a/pumpkin-world/src/generation/surface/terrain.rs b/pumpkin-world/src/generation/surface/terrain.rs index 13decd1b3..4c9fa6355 100644 --- a/pumpkin-world/src/generation/surface/terrain.rs +++ b/pumpkin-world/src/generation/surface/terrain.rs @@ -1,7 +1,10 @@ use pumpkin_data::{Block, BlockState, chunk::Biome}; use pumpkin_util::{ math::vector3::Vector3, - random::{RandomDeriver, RandomDeriverImpl, RandomGenerator, RandomImpl}, + random::{ + RandomImpl, + xoroshiro128::{Xoroshiro, XoroshiroSplitter}, + }, }; use crate::{ @@ -27,7 +30,10 @@ pub struct SurfaceTerrainBuilder { } impl SurfaceTerrainBuilder { - pub fn new(noise_builder: &DoublePerlinNoiseBuilder, random_deriver: &RandomDeriver) -> Self { + pub fn new( + noise_builder: &DoublePerlinNoiseBuilder, + random_deriver: &XoroshiroSplitter, + ) -> Self { Self { terracotta_bands: Self::create_terracotta_bands( random_deriver.split_string("minecraft:clay_bands"), @@ -56,7 +62,7 @@ impl SurfaceTerrainBuilder { RawBlockState(Block::LIGHT_GRAY_TERRACOTTA.default_state.id); const TERRACOTTA: RawBlockState = RawBlockState(Block::TERRACOTTA.default_state.id); - fn create_terracotta_bands(mut random: RandomGenerator) -> Box<[RawBlockState]> { + fn create_terracotta_bands(mut random: Xoroshiro) -> Box<[RawBlockState]> { let mut block_states = [Self::TERRACOTTA; 192]; let mut i = 0; @@ -96,7 +102,7 @@ impl SurfaceTerrainBuilder { } fn add_terracotta_bands( - random: &mut RandomGenerator, + random: &mut Xoroshiro, terracotta_bands: &mut [RawBlockState], min_band_size: i32, state: RawBlockState, @@ -189,7 +195,7 @@ impl SurfaceTerrainBuilder { estimated_surface_y: i32, current_top_y: i32, sea_level: i32, - random_deriver: &RandomDeriver, + random_deriver: &XoroshiroSplitter, ) { let iceburg_surface_noise = (self.iceberg_surface_noise.sample(x as f64, 0.0, z as f64) * 8.25).abs(); diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index ba881066f..c1bd4cfc0 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -122,7 +122,7 @@ pub fn bench_create_and_populate_noise( chunk.populate_noise( &mut noise_sampler, - random_config, + &random_config.ore_random_deriver, &mut surface_height_estimate_sampler, ); } @@ -243,7 +243,7 @@ pub fn bench_create_and_populate_noise_with_surface( chunk.populate_biomes(Dimension::OVERWORLD, &mut multi_noise_sampler); chunk.populate_noise( &mut noise_sampler, - random_config, + &random_config.ore_random_deriver, &mut surface_height_estimate_sampler, ); chunk.build_surface(