use super::channel::LevelChange; use super::chunk_holder::ChunkHolder; use super::chunk_state::{Chunk, StagedChunkEnum}; use super::dag::{DAG, EdgeKey, Node, NodeKey}; use super::generation_cache::Cache; use super::worker_logic::{RecvChunk, generation_work, io_read_work, io_write_work}; use super::{ ChunkLevel, ChunkListener, ChunkLoading, ChunkPos, HashMapType, HashSetType, IOLock, LevelChannel, }; use crate::chunk::io::Dirtiable; use crate::level::{Level, SyncChunk}; use dashmap::DashMap; use pumpkin_config::lighting::LightingEngineConfig; use pumpkin_data::chunk_gen_settings::GenerationSettings; use pumpkin_util::math::vector2::Vector2; use slotmap::Key; use std::cmp::{Ordering, max}; use std::collections::{BinaryHeap, HashMap}; use std::mem::swap; use std::sync::atomic::Ordering::Relaxed; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use tracing::{debug, error, info, trace, warn}; pub(crate) struct TaskHeapNode(i8, NodeKey); impl PartialEq for TaskHeapNode { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } impl TaskHeapNode { #[cfg(test)] pub(crate) const fn node_key(&self) -> NodeKey { self.1 } } impl Eq for TaskHeapNode {} impl PartialOrd for TaskHeapNode { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for TaskHeapNode { fn cmp(&self, other: &Self) -> Ordering { self.0.cmp(&other.0).reverse() } } pub struct GenerationSchedule { queue: BinaryHeap, graph: DAG, last_level: ChunkLevel, last_high_priority: Vec, send_level: Arc, public_chunk_map: Arc, SyncChunk>>, chunk_map: HashMap, unload_chunks: HashSetType, /// Tasks that are graph-ready (`in_degree` == 0) but cannot yet run because /// one or more of their required neighbor chunks haven't been delivered yet. /// Parked here and re-queued by `check_waiting_tasks()` as chunk data arrives. waiting_for_chunks: HashSetType, io_lock: IOLock, running_task_count: u16, max_in_flight: u16, queue_dirty: bool, recv_chunk: crossfire::compat::MRx<(ChunkPos, RecvChunk)>, io_read: crossfire::compat::MTx>, io_write: crossfire::compat::Tx>, generate: crossfire::compat::MTx<(ChunkPos, Cache, StagedChunkEnum)>, send_chunk: crossfire::compat::MTx<(ChunkPos, RecvChunk)>, gen_pool: Option>, listener: Arc, lighting_config: LightingEngineConfig, last_unload: std::time::Instant, } impl GenerationSchedule { pub fn create( io_read_thread_count: usize, gen_thread_count: usize, level: Arc, level_channel: Arc, listener: Arc, thread_tracker: &mut Vec>, gen_pool: Option>, ) { let (send_chunk, recv_chunk) = crossfire::compat::mpmc::unbounded_blocking(); let (send_read_io, recv_read_io) = crossfire::compat::mpmc::bounded_tx_blocking_rx_async(io_read_thread_count + 5); let (send_write_io, recv_write_io) = crossfire::compat::spsc::bounded_tx_blocking_rx_async(500); let (send_gen, recv_gen) = crossfire::compat::mpmc::bounded_blocking(gen_thread_count + 5); let io_lock = Arc::new(( Mutex::new(HashMapType::default()), tokio::sync::Notify::new(), )); for _ in 0..io_read_thread_count { level.chunk_system_tasks.spawn(io_read_work( recv_read_io.clone(), send_chunk.clone(), level.clone(), io_lock.clone(), )); } level.chunk_system_tasks.spawn(io_write_work( recv_write_io, level.clone(), io_lock.clone(), )); if gen_pool.is_none() { for i in 0..gen_thread_count { let recv_gen = recv_gen.clone(); let send_chunk = send_chunk.clone(); let level_clone = level.clone(); let handle = thread::Builder::new() .name(format!("Gen-{i}")) .spawn(move || { generation_work(&recv_gen, &send_chunk, &level_clone); }) .expect("Failed to spawn Generation Thread"); thread_tracker.push(handle); } } let max_in_flight = if gen_pool.is_some() { (thread::available_parallelism().map_or(1, std::num::NonZero::get) * 4) as u16 } else { gen_thread_count as u16 }; let level_sched = level; let lighting_config = level_sched.lighting_config; let handle = thread::Builder::new() .name("Schedule".to_string()) .spawn(move || { let scheduler = Self { queue: BinaryHeap::new(), graph: DAG::default(), last_level: ChunkLevel::default(), last_high_priority: Vec::new(), 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, max_in_flight, queue_dirty: false, recv_chunk, io_read: send_read_io, io_write: send_write_io, generate: send_gen, send_chunk, gen_pool, listener, chunk_map: HashMap::default(), lighting_config, last_unload: std::time::Instant::now(), }; scheduler.work(&level_sched); }) .expect("Failed to spawn Scheduler Thread"); thread_tracker.push(handle); } fn apply_lighting_override(&self, chunk: &SyncChunk) { match self.lighting_config { LightingEngineConfig::Full => { let mut engine = chunk .light_engine .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); 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_or_else(std::sync::PoisonError::into_inner); 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); } LightingEngineConfig::Default => {} } } fn calc_priority( last_level: &ChunkLevel, last_high_priority: &[ChunkPos], pos: ChunkPos, stage: StagedChunkEnum, ) -> i8 { let base_level = *last_level.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL); if base_level == ChunkLoading::MAX_LEVEL { return 127; } if last_high_priority.is_empty() { return base_level + (stage as i8); } let mut min_dst = i32::MAX; for i in last_high_priority { let dst = max((i.x - pos.x).abs(), (i.y - pos.y).abs()); min_dst = min_dst.min(dst); if dst <= StagedChunkEnum::FULL_RADIUS && stage <= StagedChunkEnum::FULL_DEPENDENCIES[dst as usize] { return base_level + (stage as i8) - 100 + (dst as i8); } } base_level + (stage as i8) + (min_dst.min(60) as i8) } fn sort_queue(&mut self) { if self.queue.is_empty() { return; } let mut tasks: Vec<_> = self.queue.drain().collect(); for i in &mut tasks { if let Some(node) = self.graph.nodes.get(i.1) { i.0 = Self::calc_priority( &self.last_level, &self.last_high_priority, node.pos, node.stage, ); } } self.queue = BinaryHeap::from(tasks); } /// TODO: will remove at some point pub(crate) fn restore_ready_tasks( graph: &mut DAG, queue: &mut BinaryHeap, chunk_map: &HashMap, last_level: &ChunkLevel, last_high_priority: &[ChunkPos], waiting_for_chunks: &HashSetType, ) -> usize { debug_assert!(queue.is_empty()); let mut ready = Vec::new(); for (key, node) in &mut graph.nodes { node.in_queue = false; if node.stage == StagedChunkEnum::None || node.in_degree != 0 || waiting_for_chunks.contains(&key) { continue; } let Some(holder) = chunk_map.get(&node.pos) else { continue; }; if holder.current_stage >= node.stage || holder.tasks[node.stage as usize] != key { continue; } ready.push((key, node.pos, node.stage)); } for (key, pos, stage) in &ready { let Some(node) = graph.nodes.get_mut(*key) else { continue; }; node.in_queue = true; queue.push(TaskHeapNode( Self::calc_priority(last_level, last_high_priority, *pos, *stage), *key, )); } ready.len() } /// 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, queue: &mut BinaryHeap, last_level: &ChunkLevel, last_high_priority: &[ChunkPos], dependency_task: NodeKey, chunk_pos: ChunkPos, holder: &mut ChunkHolder, req_stage: StagedChunkEnum, ) { // Insert occupied_by edge head holder.occupied_by = graph.edges.insert(crate::chunk_system::dag::Edge::new( dependency_task, holder.occupied_by, )); if !holder.occupied.is_null() { 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; } // 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[req_stage] must not be null before adding edge" ); graph.add_edge(ano_task, dependency_task); } /// Check if any tasks parked in `waiting_for_chunks` now have all their neighbor /// chunk data available, and re-queue them if so. /// Must be called after every `receive_chunk` call. fn check_waiting_tasks(&mut self) { if self.waiting_for_chunks.is_empty() { return; } let mut now_ready: Vec = Vec::new(); self.waiting_for_chunks.retain(|&node_key| { let Some(node) = self.graph.nodes.get(node_key) else { return false; // node was dropped, discard silently }; let write_radius = node.stage.get_write_radius(); let pos = node.pos; let all_ready = (-write_radius..=write_radius).all(|dx| { (-write_radius..=write_radius).all(|dy| { self.chunk_map .get(&pos.add_raw(dx, dy)) .is_some_and(|h| h.chunk.is_some()) }) }); if all_ready { now_ready.push(node_key); false } else { true } }); for node_key in now_ready { if let Some(n) = self.graph.nodes.get_mut(node_key) && n.in_degree == 0 && !n.in_queue { n.in_queue = true; let priority = Self::calc_priority(&self.last_level, &self.last_high_priority, n.pos, n.stage); self.queue.push(TaskHeapNode(priority, node_key)); } // If in_degree > 0, drop_node will re-queue when unblocked } } #[expect(clippy::too_many_lines)] fn resort_work(&mut self, new_data: (Option, Option>)) -> bool { if new_data.0.is_none() && new_data.1.is_none() { return false; } if let Some(high_priority) = new_data.1 { self.last_high_priority = high_priority; self.queue_dirty = true; } let Some(new_level) = new_data.0 else { return true; }; for (pos, (old_stage, new_stage)) in new_level.0 { debug_assert_ne!(old_stage, new_stage); debug_assert_eq!( new_stage, StagedChunkEnum::level_to_stage( *new_level.1.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL) ) ); let mut holder = self.chunk_map.remove(&pos).unwrap_or_default(); debug_assert_eq!(holder.target_stage, old_stage); holder.target_stage = new_stage; // 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() { let is_in_flight = self.graph.nodes.get(*task).is_some_and(|n| n.in_flight); if !is_in_flight { self.waiting_for_chunks.remove(task); self.drop_node(*task); *task = NodeKey::null(); } } } if new_stage == StagedChunkEnum::None { if holder.dependency_stage != StagedChunkEnum::None { let has_valid_task = self.graph.prune_edge_chain(&mut holder.occupied_by); if !has_valid_task { holder.dependency_stage = StagedChunkEnum::None; } } if holder.dependency_stage == StagedChunkEnum::None { self.unload_chunks.insert(pos); } } } else { if old_stage == StagedChunkEnum::None { self.unload_chunks.remove(&pos); if holder.current_stage == StagedChunkEnum::Full && !holder.public { holder.public = true; match holder.chunk.as_ref().expect("chunk exists") { Chunk::Level(chunk) => { self.apply_lighting_override(chunk); self.public_chunk_map.insert(pos, chunk.clone()); self.listener.process_new_chunk(pos, chunk); } Chunk::Proto(_) => panic!(), } } } 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())); if !holder.occupied.is_null() { self.graph.add_edge(holder.occupied, *task); } } let task = *task; if i > 1 { let stage = StagedChunkEnum::from(i); let dependency = stage.get_direct_dependencies(); let radius = stage.get_direct_radius(); for dx in -radius..=radius { for dz in -radius..=radius { let new_pos = pos.add_raw(dx, dz); let req_stage = dependency[dx.abs().max(dz.abs()) as usize]; if new_pos == pos { Self::ensure_dependency_chain( &mut self.graph, &mut self.queue, &self.last_level, &self.last_high_priority, task, new_pos, &mut holder, req_stage, ); continue; } let ano_chunk = self.chunk_map.entry(new_pos).or_default(); Self::ensure_dependency_chain( &mut self.graph, &mut self.queue, &self.last_level, &self.last_high_priority, task, new_pos, ano_chunk, req_stage, ); } } } let node = self.graph.nodes.get_mut(task).expect("node exists"); if node.in_degree == 0 && !node.in_queue { node.in_queue = true; self.queue.push(TaskHeapNode(0, task)); } } } self.chunk_map.insert(pos, holder); } self.last_level = new_level.1; self.queue_dirty = true; true } fn recompute_dependency_stages(&mut self) { let mut required: HashMapType = HashMapType::default(); let mut worklist: Vec<(ChunkPos, StagedChunkEnum)> = self .chunk_map .iter() .filter(|(_, holder)| holder.target_stage != StagedChunkEnum::None) .map(|(pos, holder)| (*pos, holder.target_stage)) .collect(); while let Some((pos, req)) = worklist.pop() { let entry = required.entry(pos).or_insert(StagedChunkEnum::None); if *entry >= req { continue; } let start = *entry as u8 + 1; *entry = req; // Only expand the stages that were not already accounted for. for i in start..=(req as u8) { let stage = StagedChunkEnum::from(i); let radius = stage.get_direct_radius(); if radius == 0 { continue; } let dependencies = stage.get_direct_dependencies(); for dx in -radius..=radius { for dz in -radius..=radius { if dx == 0 && dz == 0 { continue; } let neighbor = pos.add_raw(dx, dz); worklist.push((neighbor, dependencies[dx.abs().max(dz.abs()) as usize])); } } } } let mut nodes_to_drop = Vec::new(); let mut newly_unused = Vec::new(); for (pos, holder) in &mut self.chunk_map { let new_dependency = required.get(pos).copied().unwrap_or(StagedChunkEnum::None); if new_dependency >= holder.dependency_stage { continue; } holder.dependency_stage = new_dependency; let effective_target = holder.target_stage.max(new_dependency); for i in (effective_target as usize + 1)..StagedChunkEnum::COUNT { let task = holder.tasks[i]; if !task.is_null() { nodes_to_drop.push((*pos, i, task)); } } if effective_target == StagedChunkEnum::None { newly_unused.push(*pos); } } self.unload_chunks.extend(newly_unused); for (pos, index, task) in nodes_to_drop { if self .graph .nodes .get(task) .is_some_and(|node| node.in_flight) { continue; } self.waiting_for_chunks.remove(&task); self.drop_node(task); if let Some(holder) = self.chunk_map.get_mut(&pos) { holder.tasks[index] = NodeKey::null(); } } self.purge_dropped_queue_entries(); } /// Drop heap entries whose node has been cancelled. They are skipped when popped, /// but a saturated queue is never drained, so without this the heap keeps every /// cancelled task of every chunk the player has flown past and `sort_queue` gets /// slower on each level change. fn purge_dropped_queue_entries(&mut self) { if self.queue.is_empty() { return; } let graph = &self.graph; let tasks: Vec<_> = self .queue .drain() .filter(|task| graph.nodes.contains_key(task.1)) .collect(); self.queue = BinaryHeap::from(tasks); } fn garbage_collect_dependencies(&mut self) { self.recompute_dependency_stages(); // Garbage collect stranded dependencies and empty holders let mut stranded = Vec::new(); let mut empty_holders = Vec::new(); for (pos, holder) in &self.chunk_map { if holder.target_stage == StagedChunkEnum::None { if holder.dependency_stage != StagedChunkEnum::None { stranded.push(*pos); } else if holder.current_stage == StagedChunkEnum::None && holder.chunk.is_none() && holder.occupied.is_null() && holder.tasks.iter().all(Key::is_null) && !holder.public { empty_holders.push(*pos); } } } for pos in stranded { let holder = self.chunk_map.get_mut(&pos).expect("holder exists"); if !holder.occupied.is_null() && self.graph.nodes.contains_key(holder.occupied) { continue; } let has_valid_task = self.graph.prune_edge_chain(&mut holder.occupied_by); if !has_valid_task { holder.dependency_stage = StagedChunkEnum::None; self.unload_chunks.insert(pos); } } for pos in empty_holders { if let Some(mut holder) = self.chunk_map.remove(&pos) { self.graph.drop_edge_chain(holder.occupied_by); holder.occupied_by = EdgeKey::null(); } } } fn process_unload_queue(&mut self) { if self.unload_chunks.is_empty() { return; } let mut unload_chunks = HashSetType::default(); swap(&mut unload_chunks, &mut self.unload_chunks); let mut chunks = Vec::with_capacity(unload_chunks.len()); for pos in unload_chunks { let Some(mut holder) = self.chunk_map.remove(&pos) else { continue; }; debug_assert_eq!(holder.target_stage, StagedChunkEnum::None); if !holder.occupied.is_null() { self.chunk_map.insert(pos, holder); self.unload_chunks.insert(pos); continue; } for task in holder.tasks { if !task.is_null() { let is_in_flight = self.graph.nodes.get(task).is_some_and(|n| n.in_flight); if !is_in_flight { self.waiting_for_chunks.remove(&task); self.drop_node(task); } } } self.graph.drop_edge_chain(holder.occupied_by); holder.occupied_by = EdgeKey::null(); if holder.public { self.public_chunk_map.remove(&pos); holder.public = false; } if let Some(tmp) = holder.chunk { match tmp { Chunk::Level(chunk) => { // Save chunk to disk if dirty if chunk.is_dirty() { chunks.push((pos, Chunk::Level(chunk))); } } Chunk::Proto(chunk) => { chunks.push((pos, Chunk::Proto(chunk))); } } } } if chunks.is_empty() { return; } let mut data = self .io_lock .0 .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); for (pos, _chunk) in &chunks { *data.entry(*pos).or_insert(0) += 1; } drop(data); if let Err(e) = self.io_write.send(chunks) { error!( "Failed to send chunks to io write thread during save (may have shut down): {:?}", e ); } } fn save_all_chunk(&mut self, save_proto_chunk: bool) { let mut chunks = Vec::with_capacity(self.chunk_map.len()); for (pos, holder) in &mut self.chunk_map { if let Some(chunk) = &holder.chunk { let should_save = match chunk { Chunk::Level(sync_chunk) => sync_chunk.is_dirty(), Chunk::Proto(proto) => { save_proto_chunk && !matches!( proto.stage, crate::chunk_system::chunk_state::StagedChunkEnum::Empty | crate::chunk_system::chunk_state::StagedChunkEnum::None ) } }; if should_save { let chunk_to_save = match chunk { Chunk::Level(sync_chunk) => Chunk::Level(sync_chunk.clone()), Chunk::Proto(_) => holder.chunk.take().expect("proto chunk exists"), }; chunks.push((*pos, chunk_to_save)); } } } if chunks.is_empty() { return; } info!( "Saving {} chunks (collected from {} holders)...", chunks.len(), self.chunk_map.len() ); let mut data = self .io_lock .0 .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); for (pos, _) in &chunks { *data.entry(*pos).or_insert(0) += 1; } drop(data); if let Err(e) = self.io_write.send(chunks) { error!("Failed to send chunks to io write thread: {:?}", e); } } fn drop_node(&mut self, node: NodeKey) { let Some(old) = self.graph.nodes.remove(node) else { return; }; let mut edge = old.edge; while !edge.is_null() { let cur = self.graph.edges.remove(edge).expect("edge exists"); if let Some(node) = self.graph.nodes.get_mut(cur.to) { debug_assert!(node.in_degree >= 1); node.in_degree -= 1; if node.in_degree == 0 && !node.in_queue { // 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; } } fn drop_satisfied_tasks(&mut self, holder: &mut ChunkHolder, stage: StagedChunkEnum) { // A neighboring generation cache may already have advanced this holder past the // returning task's stage. Drop every scheduled task satisfied by the returned data, // including that task's stale in-flight node. for task_idx in (StagedChunkEnum::None as usize + 1)..=(stage as usize) { if !holder.tasks[task_idx].is_null() { self.waiting_for_chunks.remove(&holder.tasks[task_idx]); self.drop_node(holder.tasks[task_idx]); holder.tasks[task_idx] = NodeKey::null(); } } } #[expect(clippy::too_many_lines)] fn receive_chunk(&mut self, pos: ChunkPos, data: RecvChunk) { match data { RecvChunk::IO(chunk) => { let mut holder = self.chunk_map.remove(&pos).expect("holder exists"); if holder.chunk.is_some() { warn!( "receive_chunk(IO): holder already has chunk at {:?}; replacing", pos ); } debug_assert_eq!(holder.current_stage, StagedChunkEnum::None); let stage = StagedChunkEnum::from(chunk.get_stage_id()); self.drop_satisfied_tasks(&mut holder, stage); holder.current_stage = stage; debug_assert!(self.graph.nodes.contains_key(holder.occupied)); self.drop_node(holder.occupied); holder.occupied = NodeKey::null(); match &chunk { Chunk::Level(data) => { self.apply_lighting_override(data); let result = self.public_chunk_map.insert(pos, data.clone()); if result.is_some() { warn!( "receive_chunk(IO): replacing existing public chunk at {:?}", pos ); } holder.public = true; trace!( "Notifying players: chunk {:?} loaded from disk (Full status)", pos ); self.listener.process_new_chunk(pos, data); } Chunk::Proto(_) => { if holder.public { debug!( "Chunk {:?} downgraded to Proto for relighting, marking as non-public", pos ); self.public_chunk_map.remove(&pos); holder.public = false; } } } // If this chunk was only loaded for a dependency or cancelled // and is no longer needed, clear dependency_stage and queue unload. if holder.target_stage == StagedChunkEnum::None && holder.current_stage >= holder.dependency_stage { holder.dependency_stage = StagedChunkEnum::None; self.unload_chunks.insert(pos); } 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) => { let mut dx = 0; let mut dy = 0; for chunk in data.chunks { let new_pos = ChunkPos::new(data.x + dx, data.z + dy); match chunk { Chunk::Level(chunk) => { let mut holder = self.chunk_map.remove(&new_pos).expect("holder exists"); let stage = StagedChunkEnum::Full; if new_pos == pos { if holder.current_stage != StagedChunkEnum::Spawn { warn!( "receive_chunk(Level): holder at {:?} for pos {:?} expected {:?}; aligning", holder.current_stage, new_pos, StagedChunkEnum::Spawn ); holder.current_stage = StagedChunkEnum::Spawn; } self.drop_satisfied_tasks(&mut holder, stage); if self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } holder.current_stage = stage; let was_public = holder.public; self.apply_lighting_override(&chunk); let public_chunk = chunk.clone(); if was_public { self.public_chunk_map.insert(new_pos, public_chunk); info!( "Notifying players: regenerated chunk at {:?} (was already public)", new_pos ); self.listener.process_new_chunk(new_pos, &chunk); holder.chunk = Some(Chunk::Level(chunk)); } else { holder.chunk = Some(Chunk::Level(chunk)); let result = self.public_chunk_map.insert(new_pos, public_chunk); holder.public = true; if result.is_some() { warn!( "public_chunk_map.insert returned existing chunk for {new_pos:?}" ); } if let Some(pc) = self.public_chunk_map.get(&new_pos) { trace!( "Notifying players: new chunk at {:?} (generation complete)", new_pos ); self.listener.process_new_chunk(new_pos, &pc); } else { error!( "CRITICAL: Failed to retrieve chunk {:?} from public_chunk_map immediately after insert!", new_pos ); } } } else { self.drop_satisfied_tasks(&mut holder, stage); holder.current_stage = stage; holder.chunk = Some(Chunk::Level(chunk)); } if !holder.occupied.is_null() && self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } holder.occupied = NodeKey::null(); // If this chunk was only loaded for a dependency or cancelled // and is no longer needed, clear dependency_stage and queue unload. if holder.target_stage == StagedChunkEnum::None && 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).expect("holder exists"); let stage = StagedChunkEnum::from(chunk.stage_id()); self.drop_satisfied_tasks(&mut holder, stage); if new_pos == pos { debug_assert_ne!(holder.current_stage, StagedChunkEnum::None); if self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } holder.current_stage = stage; } else { if holder.current_stage < stage { holder.current_stage = 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(); holder.chunk = Some(Chunk::Proto(chunk)); self.chunk_map.insert(new_pos, holder); } } dy += 1; if dy == data.size { dy = 0; dx += 1; } } // Neighbor chunks returned to holders — unblock waiting tasks self.check_waiting_tasks(); } RecvChunk::GenerationFailure { pos: fail_pos, stage, error, } => { error!( "Received generation failure notification for chunk {:?} at stage {:?}: {}", fail_pos, stage, error ); if let Some(mut holder) = self.chunk_map.remove(&pos) { let target_stage = holder.target_stage; if !holder.occupied.is_null() { if self.graph.nodes.contains_key(holder.occupied) { self.drop_node(holder.occupied); } holder.occupied = NodeKey::null(); } 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(); } } holder.current_stage = StagedChunkEnum::None; holder.dependency_stage = StagedChunkEnum::None; holder.chunk = None; 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); if i > (StagedChunkEnum::None as usize + 1) { self.graph.add_edge(holder.tasks[i - 1], holder.tasks[i]); } } 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) { node.in_queue = true; } self.queue.push(TaskHeapNode( Self::calc_priority( &self.last_level, &self.last_high_priority, pos, StagedChunkEnum::from(1), ) - 50, first_task, )); } self.chunk_map.insert(pos, holder); warn!( "Chunk {:?} reset to None and re-queued for regeneration (target: {:?})", pos, target_stage ); } else { error!("Failed to find holder for failed chunk {:?}", pos); } } } self.running_task_count -= 1; } #[expect(clippy::too_many_lines)] fn work(mut self, level: &Arc) { debug!( "schedule thread start id: {:?} name: {}", thread::current().id(), thread::current().name().unwrap_or("unknown") ); loop { if level.should_unload.swap(false, Relaxed) { self.garbage_collect_dependencies(); self.process_unload_queue(); } if level.should_save.swap(false, Relaxed) { self.save_all_chunk(false); } if level.shut_down_chunk_system.load(Relaxed) { info!("Saving chunks before shutdown..."); self.garbage_collect_dependencies(); self.process_unload_queue(); self.save_all_chunk(true); break; } // 1. Get latest world state (player moves, etc) if self.resort_work(self.send_level.get()) { self.garbage_collect_dependencies(); } // Process unload queue periodically (every 1 second) to batch writes together // and act as a brief memory cache if a player walks back into the chunk. // This must run even when the queue is empty: `garbage_collect_dependencies` // is what puts stale dependency holders into the queue in the first place. if self.last_unload.elapsed() >= std::time::Duration::from_secs(1) { self.garbage_collect_dependencies(); self.process_unload_queue(); self.last_unload = std::time::Instant::now(); } // 2. Process all pending chunk results from workers while let Ok((pos, data)) = self.recv_chunk.try_recv() { self.receive_chunk(pos, data); } // 3. Re-sort if world state changed or new tasks added if self.queue_dirty { self.sort_queue(); self.queue_dirty = false; } // 4. Process ready tasks in the queue (up to max_in_flight) let mut io_batch = Vec::with_capacity(16); 'out2: while let Some(task) = self.queue.pop() { if level.shut_down_chunk_system.load(Relaxed) { self.queue.push(task); info!("Shutdown detected during task processing, saving chunks..."); self.save_all_chunk(true); break 'out2; } if self.running_task_count >= self.max_in_flight { self.queue.push(task); break 'out2; } // Briefly check for high-priority results or world changes to avoid stalling while let Ok((pos, data)) = self.recv_chunk.try_recv() { self.receive_chunk(pos, data); if self.resort_work(self.send_level.get()) { // If world state changed, we MUST re-sort before continuing self.garbage_collect_dependencies(); self.queue.push(task); self.queue_dirty = true; break 'out2; } } if let Some(node) = self.graph.nodes.get_mut(task.1) { if node.in_degree != 0 { node.in_queue = false; continue; } node.in_flight = true; let node = node.clone(); // A chunk can be advanced as part of a neighboring task's write cache. // In that case its queued node may survive even though the returned // ProtoChunk has already reached this stage. Dispatching the stale node // would run the same stage twice and trip ProtoChunk's stage invariant. let actual_stage = self .chunk_map .get(&node.pos) .and_then(|holder| holder.chunk.as_ref()) .map(Chunk::get_stage_id); if actual_stage.is_some_and(|stage| stage >= node.stage as u8) { if let Some(holder) = self.chunk_map.get_mut(&node.pos) { holder.current_stage = holder .current_stage .max(StagedChunkEnum::from(actual_stage.expect("checked above"))); let task_slot = &mut holder.tasks[node.stage as usize]; if *task_slot == task.1 { *task_slot = NodeKey::null(); } } self.waiting_for_chunks.remove(&task.1); self.drop_node(task.1); continue; } // Cancel/drop task if chunk is out of range or no longer needed by any target/dependency let effective_target = self .chunk_map .get(&node.pos) .map_or(StagedChunkEnum::None, |h| { h.target_stage.max(h.dependency_stage) }); if node.stage > effective_target { self.waiting_for_chunks.remove(&task.1); self.drop_node(task.1); continue; } if node.stage == StagedChunkEnum::Empty { self.running_task_count += 1; let holder = self.chunk_map.get_mut(&node.pos).expect("holder exists"); debug_assert!(holder.occupied.is_null()); debug_assert_eq!(holder.current_stage, StagedChunkEnum::None); let occupy = self.graph.nodes.insert(Node::new( ChunkPos::new(i32::MAX, i32::MAX), StagedChunkEnum::None, )); 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; io_batch.push(node.pos); if io_batch.len() >= 16 && self.io_read.send(std::mem::take(&mut io_batch)).is_err() { info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); break 'out2; } } else { // Send any pending IO batch before starting generation if !io_batch.is_empty() && self.io_read.send(std::mem::take(&mut io_batch)).is_err() { info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); break 'out2; } let write_radius = node.stage.get_write_radius(); // 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; n.in_flight = 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, ); let occupy = self.graph.nodes.insert(Node::new( ChunkPos::new(i32::MAX, i32::MAX), StagedChunkEnum::None, )); for dx in -write_radius..=write_radius { for dy in -write_radius..=write_radius { let new_pos = node.pos.add_raw(dx, dy); let holder = self.chunk_map.get_mut(&new_pos).expect("holder exists"); let mut tmp = None; swap(&mut tmp, &mut holder.chunk); let Some(tmp) = tmp else { panic!( "Missing chunk for position {:?} while processing generation task for {:?} stage {:?}", new_pos, node.pos, node.stage ) }; match tmp { Chunk::Level(chunk) => { cache.chunks.push(Chunk::Level(chunk)); } Chunk::Proto(chunk) => { cache.chunks.push(Chunk::Proto(chunk)); } } debug_assert!(holder.occupied.is_null()); let mut cur_edge = holder.occupied_by; let mut prev_edge = EdgeKey::null(); let mut change_head = None; while !cur_edge.is_null() { let edge = self.graph.edges.get(cur_edge).expect("edge exists"); if self.graph.nodes.contains_key(edge.to) { prev_edge = cur_edge; cur_edge = edge.next; self.graph.add_edge(occupy, edge.to); } else { let next = edge.next; self.graph.edges.remove(cur_edge); cur_edge = next; if prev_edge.is_null() { change_head = Some(next); } else { self.graph .edges .get_mut(prev_edge) .expect("edge exists") .next = next; } } } if let Some(next) = change_head { holder.occupied_by = next; } holder.occupied = occupy; } } self.running_task_count += 1; if let Some(pool) = &self.gen_pool { let pos = node.pos; let stage = node.stage; let send_chunk = self.send_chunk.clone(); let level = level.clone(); let settings = GenerationSettings::from_dimension(level.world_gen.dimension()); pool.spawn(move || { let result = crate::chunk_system::worker_logic::run_generation( pos, cache, stage, &level, settings, ); let _ = send_chunk.send((pos, result)); }); } else if self.generate.send((node.pos, cache, node.stage)).is_err() { self.running_task_count = self.running_task_count.saturating_sub(1); info!("Generation thread closed, saving remaining chunks..."); self.save_all_chunk(true); break 'out2; } } } } // Flush any remaining IO batch if !io_batch.is_empty() && self.io_read.send(std::mem::take(&mut io_batch)).is_err() { info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); } // 3. If queue is empty, wait for work or results if self.queue.is_empty() { // If we have tasks in flight, wait for them with timeout if self.running_task_count > 0 || !self.waiting_for_chunks.is_empty() { match self.recv_chunk.recv_timeout(Duration::from_millis(5)) { Ok((pos, data)) => { self.receive_chunk(pos, data); if self.resort_work(self.send_level.get()) { self.garbage_collect_dependencies(); } } Err(crossfire::compat::RecvTimeoutError::Timeout) => { // Periodically check LevelChannel for new requests if self.resort_work(self.send_level.get()) { self.garbage_collect_dependencies(); } } Err(crossfire::compat::RecvTimeoutError::Disconnected) => break, } } else { // No tasks in flight, wait indefinitely for LevelChannel changes let restored = Self::restore_ready_tasks( &mut self.graph, &mut self.queue, &self.chunk_map, &self.last_level, &self.last_high_priority, &self.waiting_for_chunks, ); if restored > 0 { warn!("Restored {restored} stranded ready chunk tasks to generation queue"); continue; } debug_assert!(self.debug_check()); debug_assert_eq!(self.running_task_count, 0); if self.resort_work(self.send_level.wait_and_get(level)) { self.garbage_collect_dependencies(); } } if self.queue_dirty { self.sort_queue(); self.queue_dirty = false; } } } info!( "schedule: waiting for {} generation tasks to finish", self.running_task_count ); let mut wait_iterations = 0; let max_wait_iterations = 100; // 5 seconds max wait 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; } else { wait_iterations += 1; if wait_iterations % 20 == 0 { warn!( "Still waiting for {} tasks to complete (waited {}ms)", self.running_task_count, wait_iterations * 50 ); } thread::sleep(Duration::from_millis(50)); } } if self.running_task_count > 0 { warn!( "Cancelling {} in-flight generation tasks", self.running_task_count ); let mut nodes_to_drop = Vec::new(); for holder in self.chunk_map.values_mut() { for task in &mut holder.tasks { if !task.is_null() { self.waiting_for_chunks.remove(task); nodes_to_drop.push(*task); *task = NodeKey::null(); } } if !holder.occupied.is_null() && let Some(node) = self.graph.nodes.get(holder.occupied) && node.pos.x == i32::MAX && node.pos.y == i32::MAX { nodes_to_drop.push(holder.occupied); holder.occupied = NodeKey::null(); } self.graph.drop_edge_chain(holder.occupied_by); holder.occupied_by = EdgeKey::null(); } for node_key in nodes_to_drop { self.drop_node(node_key); } self.running_task_count = 0; } drop(self.io_write); let unreleased_count = self.graph.nodes.len(); if unreleased_count > 0 { warn!( "Cleaning up {} unreleased nodes from incomplete tasks", unreleased_count ); } self.graph.edges.clear(); } fn debug_check(&self) -> bool { if !self.graph.nodes.is_empty() { for (key, value) in &self.graph.nodes { error!("unrelease node {key:?}: {value:?}"); } panic!("nodes count error"); } for (pos, holder) in &self.chunk_map { for i in &holder.tasks { debug_assert!(i.is_null()); } debug_assert_eq!( holder.target_stage, StagedChunkEnum::level_to_stage( *self.last_level.get(pos).unwrap_or(&ChunkLoading::MAX_LEVEL) ) ); 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!( holder.chunk.as_ref().expect("chunk exists").get_stage_id(), holder.current_stage as u8 ); } } true } }