fix: structures
This commit is contained in:
parent
3392aa164f
commit
d41e5b9b98
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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) => {{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user