1605 lines
67 KiB
Rust
1605 lines
67 KiB
Rust
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<Ordering> {
|
|
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<TaskHeapNode>,
|
|
graph: DAG,
|
|
|
|
last_level: ChunkLevel,
|
|
last_high_priority: Vec<ChunkPos>,
|
|
send_level: Arc<LevelChannel>,
|
|
|
|
public_chunk_map: Arc<DashMap<Vector2<i32>, SyncChunk>>,
|
|
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,
|
|
max_in_flight: u16,
|
|
queue_dirty: bool,
|
|
recv_chunk: crossfire::compat::MRx<(ChunkPos, RecvChunk)>,
|
|
io_read: crossfire::compat::MTx<Vec<ChunkPos>>,
|
|
io_write: crossfire::compat::Tx<Vec<(ChunkPos, Chunk)>>,
|
|
generate: crossfire::compat::MTx<(ChunkPos, Cache, StagedChunkEnum)>,
|
|
send_chunk: crossfire::compat::MTx<(ChunkPos, RecvChunk)>,
|
|
gen_pool: Option<Arc<rayon::ThreadPool>>,
|
|
listener: Arc<ChunkListener>,
|
|
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>,
|
|
level_channel: Arc<LevelChannel>,
|
|
listener: Arc<ChunkListener>,
|
|
thread_tracker: &mut Vec<thread::JoinHandle<()>>,
|
|
gen_pool: Option<Arc<rayon::ThreadPool>>,
|
|
) {
|
|
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<TaskHeapNode>,
|
|
chunk_map: &HashMap<ChunkPos, ChunkHolder>,
|
|
last_level: &ChunkLevel,
|
|
last_high_priority: &[ChunkPos],
|
|
waiting_for_chunks: &HashSetType<NodeKey>,
|
|
) -> 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<TaskHeapNode>,
|
|
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<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
|
|
}
|
|
}
|
|
|
|
#[expect(clippy::too_many_lines)]
|
|
fn resort_work(&mut self, new_data: (Option<LevelChange>, Option<Vec<ChunkPos>>)) -> 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<ChunkPos, StagedChunkEnum> = 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<Level>) {
|
|
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.load().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
|
|
}
|
|
}
|