fix: structures

This commit is contained in:
Alexander Medvedev 2026-03-06 22:01:39 +01:00
parent 3392aa164f
commit d41e5b9b98
24 changed files with 374 additions and 247 deletions

View File

@ -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,
};

View File

@ -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))
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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<Chunk>,
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(),

View File

@ -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],

View File

@ -59,6 +59,11 @@ pub struct GenerationSchedule {
chunk_map: HashMap<ChunkPos, ChunkHolder>,
unload_chunks: HashSetType<ChunkPos>,
/// 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<NodeKey>,
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<NodeKey> = 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<LevelChange>, Option<Vec<ChunkPos>>)) -> 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!(

View File

@ -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?

View File

@ -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<VanillaGenerator>
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")

View File

@ -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<GlobalRandomConfig> =
LazyLock::new(|| GlobalRandomConfig::new(SEED, false));
LazyLock::new(|| GlobalRandomConfig::new(SEED));
static PROTO_ROUTER: LazyLock<ProtoNoiseRouters> = LazyLock::new(|| {
let router_ast = &OVERWORLD_BASE_NOISE_ROUTER;
ProtoNoiseRouters::generate(router_ast, &RANDOM_CONFIG)

View File

@ -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<i32>,
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<i32>,
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,

View File

@ -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<i32>,
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 {

View File

@ -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,

View File

@ -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,

View File

@ -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];

View File

@ -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<GlobalRandomConfig> =
LazyLock::new(|| GlobalRandomConfig::new(SEED, false));
LazyLock::new(|| GlobalRandomConfig::new(SEED));
macro_rules! build_function_stack {
($stack:expr) => {{

View File

@ -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);

View File

@ -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::<ProtoNoiseFunctionComponent>::with_capacity(base_stack.len());

View File

@ -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(),

View File

@ -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,

View File

@ -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();

View File

@ -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(