Skip to main content

steel_core/chunk/chunk_map/
generation_readiness.rs

1use super::{
2    Arc, ChunkGenerationTask, ChunkHolder, ChunkMap, ChunkPos, ChunkStatus, ChunkTicketLevel,
3    FullNeighborhoodCounts, FullNeighborhoodError, FullNeighborhoodIndex, FullPublication,
4    FxHashMap, GENERATION_THREAD_MULTIPLE, GenerationTaskPriority, Instant, LoadLevelChange,
5    Ordering, PackedChunkPos, PostProcessGenerationError, ReadinessReconcileResult,
6    RunningGenerationTaskPermit, TickableChunk, TickingChunkSnapshot, TickingReadiness,
7    TickingReadinessCandidate, instrument, is_block_ticking, is_entity_ticking, is_full,
8};
9
10impl ChunkMap {
11    /// Schedules a new generation task.
12    #[inline]
13    #[instrument(level = "trace", skip(self), fields(chunk = ?pos, target = ?target_status))]
14    pub(crate) fn schedule_generation_task_b(
15        self: &Arc<Self>,
16        target_status: ChunkStatus,
17        pos: ChunkPos,
18    ) -> Arc<ChunkGenerationTask> {
19        let task = Arc::new(ChunkGenerationTask::new(
20            pos,
21            target_status,
22            Arc::clone(self),
23            Arc::clone(&self.generation_pool),
24            self.cancel_token.child_token(),
25        ));
26        self.pending_generation_tasks.lock().push(Arc::clone(&task));
27        task
28    }
29
30    /// Runs queued generation tasks.
31    #[instrument(level = "trace", skip(self))]
32    pub fn run_generation_tasks_b(&self) {
33        if self.generation_refill_stopped.load(Ordering::Acquire) {
34            return;
35        }
36
37        let mut pending = self.pending_generation_tasks.lock();
38        if pending.is_empty() {
39            return;
40        }
41
42        pending.retain(|task| !task.is_cancelled());
43        if pending.is_empty() {
44            return;
45        }
46
47        let running_tasks = self.running_generation_tasks.load(Ordering::Acquire);
48        let max_running_tasks = self.max_running_generation_tasks();
49        let available_slots = max_running_tasks.saturating_sub(running_tasks);
50        if available_slots == 0 {
51            tracing::trace!(
52                pending = pending.len(),
53                running_tasks,
54                max_running_tasks,
55                "Generation task cap reached"
56            );
57            return;
58        }
59
60        let task_count = pending.len().min(available_slots);
61        if task_count < pending.len() {
62            pending.sort_by_cached_key(|task| Self::generation_task_priority(task));
63        }
64
65        tracing::trace!(
66            task_count,
67            pending = pending.len(),
68            running_tasks,
69            max_running_tasks,
70            "Running generation tasks"
71        );
72        let tasks = pending.drain(..task_count).collect::<Vec<_>>();
73        self.running_generation_tasks
74            .fetch_add(tasks.len(), Ordering::AcqRel);
75        drop(pending); // Release lock before spawning
76
77        for task in tasks {
78            let permit = RunningGenerationTaskPermit {
79                chunk_map: Arc::clone(&task.chunk_map),
80                task: Arc::clone(&task),
81            };
82            self.task_tracker.spawn_on(
83                async move {
84                    let _permit = permit;
85                    task.run().await;
86                },
87                self.chunk_runtime.handle(),
88            );
89        }
90    }
91
92    pub(super) fn max_running_generation_tasks(&self) -> usize {
93        self.generation_pool.current_num_threads().max(1) * GENERATION_THREAD_MULTIPLE
94    }
95
96    pub(super) fn generation_task_priority(task: &ChunkGenerationTask) -> GenerationTaskPriority {
97        let holder = task.center_holder();
98        GenerationTaskPriority::for_levels(holder.load_level(), holder.simulation_level())
99    }
100
101    /// Updates scheduling for a chunk based on its new level.
102    ///
103    /// Returns the holder for every loaded level and `None` only when unloading, so every
104    /// position granted a level owns a live holder before `ChunkGenerationTask::new` reads it.
105    #[inline]
106    pub(super) fn update_chunk_level(
107        self: &Arc<Self>,
108        pos: ChunkPos,
109        new_level: Option<ChunkTicketLevel>,
110    ) -> Option<Arc<ChunkHolder>> {
111        // Recover from unloading if possible, else create new holder.
112        let (chunk_holder, initialize_simulation) =
113            if let Some(holder) = self.chunks.read_sync(&pos, |_, holder| Arc::clone(holder)) {
114                (holder, false)
115            } else {
116                let level = new_level?;
117
118                if let Some(entry) = self.unloading_chunks.remove_sync(&pos) {
119                    let holder = entry.1;
120                    holder.revive_from_unloading();
121                    let _ = self.chunks.insert_sync(pos, Arc::clone(&holder));
122                    (holder, true)
123                } else {
124                    let holder = Arc::new(ChunkHolder::new_with_full_publications(
125                        pos,
126                        level,
127                        None,
128                        self.world_gen_context.min_y(),
129                        self.world_gen_context.height(),
130                        Arc::downgrade(&self.full_publications),
131                    ));
132                    let _ = self.chunks.insert_sync(pos, Arc::clone(&holder));
133                    (holder, true)
134                }
135            };
136
137        let Some(level) = new_level else {
138            self.begin_chunk_unload(pos, &chunk_holder);
139            return None;
140        };
141
142        let old = chunk_holder.swap_load_level(level);
143        if initialize_simulation {
144            chunk_holder.set_simulation_level(self.scheduling.simulation_level(pos));
145        }
146        if old != Some(level) {
147            chunk_holder.update_highest_allowed_status(Some(level));
148        }
149        if chunk_holder.try_chunk(ChunkStatus::Empty).is_some() {
150            let world = self.world_gen_context.world();
151            world.on_entity_chunk_loaded(pos);
152            world.update_entity_chunk_visibility(pos, chunk_holder.entity_visibility());
153        }
154        if is_full(level)
155            && !old.is_some_and(is_full)
156            && chunk_holder.is_full_status_initialized()
157            && chunk_holder.published_status() == Some(ChunkStatus::Full)
158            && chunk_holder.try_chunk(ChunkStatus::Full).is_some()
159        {
160            self.full_publications.publish(&chunk_holder);
161        }
162        Some(chunk_holder)
163    }
164
165    /// Tears an active holder down and moves it to `unloading_chunks` for deferred unload.
166    fn begin_chunk_unload(&self, pos: ChunkPos, chunk_holder: &Arc<ChunkHolder>) {
167        chunk_holder.begin_unloading();
168        chunk_holder.cancel_generation_task();
169        chunk_holder.clear_load_level();
170        chunk_holder.set_simulation_level(None);
171        chunk_holder.update_highest_allowed_status(None);
172        // Wake any await_chunk futures so generation tasks holding refs to
173        // this chunk can detect the status is disallowed and exit.
174        chunk_holder.wake_all_watchers();
175
176        // Clean up POI data for this chunk column
177        let world = self.world_gen_context.world();
178        world.on_entity_chunk_unload_start(pos);
179        world.poi_storage.lock().remove_chunk(pos);
180
181        if let Some(chunk) = chunk_holder.try_full_chunk() {
182            chunk.suspend_block_entities(chunk_holder);
183        }
184
185        if let Some((_, holder)) = self.chunks.remove_sync(&pos) {
186            let _ = self.unloading_chunks.insert_sync(pos, holder);
187        }
188    }
189
190    pub(super) fn prepare_ticking_readiness_demotions(
191        &self,
192        changes: &[LoadLevelChange],
193    ) -> Result<bool, FullNeighborhoodError> {
194        if changes.is_empty() {
195            return Ok(false);
196        }
197
198        let new_levels = changes
199            .iter()
200            .map(|change| (change.pos, change.new_level))
201            .collect::<FxHashMap<_, _>>();
202        let active_changes = changes
203            .iter()
204            .filter_map(|change| {
205                self.chunks
206                    .read_sync(&change.pos, |_, holder| Arc::clone(holder))
207                    .map(|holder| (change.pos, holder, change.new_level))
208            })
209            .collect::<Vec<_>>();
210
211        let dirty = {
212            let mut neighborhood = self.full_neighborhood.lock();
213            for (pos, holder, new_level) in &active_changes {
214                if !new_level.is_some_and(is_full) {
215                    neighborhood.remove_contributor_if_matches(*pos, holder)?;
216                }
217            }
218            for change in changes {
219                neighborhood.mark_dirty(change.pos);
220            }
221            neighborhood.dirty_counts_snapshot()
222        };
223
224        let candidates = self.readiness_candidates(&dirty, Some(&new_levels));
225        let snapshot_changed = self.apply_readiness_demotions(&candidates);
226        self.update_pending_readiness(&candidates);
227        Ok(snapshot_changed)
228    }
229
230    #[cfg(test)]
231    pub(super) fn reconcile_ticking_readiness(
232        &self,
233        changed_positions: &[ChunkPos],
234    ) -> Result<bool, FullNeighborhoodError> {
235        self.reconcile_ticking_readiness_measured(changed_positions)
236            .map(|result| result.snapshot_changed)
237    }
238
239    pub(super) fn reconcile_ticking_readiness_measured(
240        &self,
241        changed_positions: &[ChunkPos],
242    ) -> Result<ReadinessReconcileResult, FullNeighborhoodError> {
243        let publications = self.full_publications.drain();
244        if publications.is_empty() && changed_positions.is_empty() {
245            return Ok(ReadinessReconcileResult::default());
246        }
247        let mut contributor_updates = FxHashMap::default();
248        let mut activation_holders = FxHashMap::default();
249
250        for &pos in changed_positions {
251            contributor_updates.insert(pos, self.current_full_contributor(pos));
252        }
253        for publication in publications {
254            if let Some(holder) = self.validate_full_publication(&publication) {
255                contributor_updates.insert(publication.pos, Some(Arc::clone(&holder)));
256                activation_holders.insert(publication.pos, holder);
257            }
258        }
259
260        let dirty = {
261            let mut neighborhood = self.full_neighborhood.lock();
262            for &pos in changed_positions {
263                neighborhood.mark_dirty(pos);
264            }
265            for (pos, holder) in &contributor_updates {
266                neighborhood.reconcile_contributor(*pos, holder.as_ref())?;
267            }
268            neighborhood.take_dirty_counts()
269        };
270
271        let mut activation_holders = activation_holders.into_values().collect::<Vec<_>>();
272        // Vanilla runs each Full-load callback from the main-executor task whose
273        // ordering depends on asynchronous completion history. Steel deliberately
274        // stabilizes this load-only cross-chunk tie by packed chunk position while
275        // retaining each chunk's native block-entity map order.
276        activation_holders.sort_unstable_by_key(|holder| PackedChunkPos::from(holder.get_pos()));
277        self.activate_block_entities(&activation_holders);
278        Ok(self.apply_final_readiness(dirty))
279    }
280
281    /// Rebuilds every gameplay ticking view from one optimized SCC traversal.
282    ///
283    /// This runs only after lifecycle/readiness changes, never as fixed per-tick
284    /// bookkeeping. The published snapshot owns holders but never component guards,
285    /// so callbacks cannot retain section or chunk-component locks.
286    pub(crate) fn rebuild_ticking_chunk_snapshot(&self) -> usize {
287        let mut block = Vec::new();
288        let mut random_chunk_indices = Vec::new();
289        let mut entity_indices = Vec::new();
290
291        self.chunks.iter_sync(|pos, holder| {
292            let Some(simulation_level) = holder.simulation_level() else {
293                return true;
294            };
295            let readiness = holder.ticking_readiness_snapshot();
296            if !simulation_level.is_block_ticking() || !readiness.is_block_ticking() {
297                return true;
298            }
299            let Some(full) = holder.try_full_chunk() else {
300                return true;
301            };
302            let randomly_ticking_sections =
303                Arc::clone(full.common().sections.random_tick_sections());
304
305            let index = block.len();
306            block.push(TickableChunk {
307                pos: *pos,
308                holder: Arc::clone(holder),
309                randomly_ticking_sections,
310            });
311            if simulation_level.is_entity_ticking() {
312                random_chunk_indices.push(index);
313                if readiness.is_entity_ticking() {
314                    entity_indices.push(index);
315                }
316            }
317            true
318        });
319
320        if let Err(error) = self
321            .world_gen_context
322            .world()
323            .reconcile_active_scheduled_tick_chunks(block.iter().map(|chunk| chunk.pos))
324        {
325            panic!(
326                "Full chunk scheduled-tick ownership invariant failed during ticking snapshot rebuild: {error:?}"
327            );
328        }
329
330        let ticking_chunk_count = block.len();
331        self.ticking_chunks.store(Arc::new(TickingChunkSnapshot {
332            block: block.into_boxed_slice(),
333            random_chunk_indices: random_chunk_indices.into_boxed_slice(),
334            entity_indices: entity_indices.into_boxed_slice(),
335        }));
336        ticking_chunk_count
337    }
338
339    pub(super) fn ticking_snapshot_membership(
340        readiness: TickingReadiness,
341        simulation_level: Option<ChunkTicketLevel>,
342    ) -> (bool, bool, bool) {
343        let block = simulation_level.is_some_and(ChunkTicketLevel::is_block_ticking)
344            && readiness != TickingReadiness::Unready;
345        let random = block && simulation_level.is_some_and(ChunkTicketLevel::is_entity_ticking);
346        let entity = random && readiness == TickingReadiness::EntityTicking;
347        (block, random, entity)
348    }
349
350    // Readiness bookkeeping scans candidates once. Keep these lookups uncached so the
351    // four-entry gameplay cache remains focused on repeated callback locality.
352    pub(super) fn current_full_contributor(&self, pos: ChunkPos) -> Option<Arc<ChunkHolder>> {
353        let holder = self
354            .chunks
355            .read_sync(&pos, |_, holder| Arc::clone(holder))?;
356        if !holder.load_level().is_some_and(is_full)
357            || !holder.is_full_status_initialized()
358            || holder.published_status() != Some(ChunkStatus::Full)
359            || holder.try_chunk(ChunkStatus::Full).is_none()
360        {
361            return None;
362        }
363        Some(holder)
364    }
365
366    pub(super) fn validate_full_publication(
367        &self,
368        publication: &FullPublication,
369    ) -> Option<Arc<ChunkHolder>> {
370        let published_holder = publication.holder.upgrade()?;
371        let active_holder = self
372            .chunks
373            .read_sync(&publication.pos, |_, holder| Arc::clone(holder))?;
374        if !Arc::ptr_eq(&published_holder, &active_holder)
375            || !active_holder.load_level().is_some_and(is_full)
376            || !active_holder.is_full_status_initialized()
377            || active_holder.published_status() != Some(ChunkStatus::Full)
378            || active_holder.try_chunk(ChunkStatus::Full).is_none()
379        {
380            return None;
381        }
382        Some(active_holder)
383    }
384
385    pub(super) fn readiness_candidates(
386        &self,
387        dirty: &[(ChunkPos, FullNeighborhoodCounts)],
388        new_levels: Option<&FxHashMap<ChunkPos, Option<ChunkTicketLevel>>>,
389    ) -> Vec<TickingReadinessCandidate> {
390        dirty
391            .iter()
392            .filter_map(|(pos, counts)| {
393                let holder = self.chunks.read_sync(pos, |_, holder| Arc::clone(holder))?;
394                let load_level = match new_levels.and_then(|levels| levels.get(pos)) {
395                    Some(level) => *level,
396                    None => holder.load_level(),
397                };
398                let desired = Self::desired_ticking_readiness(load_level);
399                let target = Self::target_ticking_readiness(&holder, load_level, *counts);
400                Some(TickingReadinessCandidate {
401                    pos: *pos,
402                    holder,
403                    desired,
404                    target,
405                })
406            })
407            .collect()
408    }
409
410    const fn desired_ticking_readiness(level: Option<ChunkTicketLevel>) -> TickingReadiness {
411        if is_entity_ticking(level) {
412            TickingReadiness::EntityTicking
413        } else if is_block_ticking(level) {
414            TickingReadiness::BlockTicking
415        } else {
416            TickingReadiness::Unready
417        }
418    }
419
420    pub(super) fn target_ticking_readiness(
421        holder: &ChunkHolder,
422        level: Option<ChunkTicketLevel>,
423        counts: FullNeighborhoodCounts,
424    ) -> TickingReadiness {
425        if !holder.is_full_status_initialized()
426            || holder.published_status() != Some(ChunkStatus::Full)
427            || holder.try_chunk(ChunkStatus::Full).is_none()
428        {
429            return TickingReadiness::Unready;
430        }
431        if is_entity_ticking(level) && counts.entity_ticking_ready() {
432            TickingReadiness::EntityTicking
433        } else if is_block_ticking(level) && counts.block_ticking_ready() {
434            TickingReadiness::BlockTicking
435        } else {
436            TickingReadiness::Unready
437        }
438    }
439
440    pub(super) fn apply_readiness_demotions(
441        &self,
442        candidates: &[TickingReadinessCandidate],
443    ) -> bool {
444        let world = self.world_gen_context.world();
445        let mut snapshot_changed = false;
446        for candidate in candidates {
447            let current = candidate.holder.ticking_readiness_snapshot().readiness();
448            if current <= candidate.target {
449                continue;
450            }
451            let Some(previous) = candidate
452                .holder
453                .transition_ticking_readiness(candidate.target)
454            else {
455                continue;
456            };
457            let simulation_level = candidate.holder.simulation_level();
458            snapshot_changed |= Self::ticking_snapshot_membership(previous, simulation_level)
459                != Self::ticking_snapshot_membership(candidate.target, simulation_level);
460            world.update_entity_chunk_visibility(
461                candidate.pos,
462                candidate.holder.entity_visibility(),
463            );
464        }
465        snapshot_changed
466    }
467
468    pub(super) fn apply_final_readiness(
469        &self,
470        dirty: Vec<(ChunkPos, FullNeighborhoodCounts)>,
471    ) -> ReadinessReconcileResult {
472        if dirty.is_empty() {
473            return ReadinessReconcileResult::default();
474        }
475
476        let candidates = self.readiness_candidates(&dirty, None);
477        let mut result = ReadinessReconcileResult {
478            snapshot_changed: self.apply_readiness_demotions(&candidates),
479            candidate_count: candidates.len(),
480            ..ReadinessReconcileResult::default()
481        };
482        self.update_pending_readiness(&candidates);
483
484        let world = self.world_gen_context.world();
485        for candidate in &candidates {
486            let current = candidate.holder.ticking_readiness_snapshot().readiness();
487            if current >= candidate.target {
488                continue;
489            }
490
491            if current == TickingReadiness::Unready {
492                let start = Instant::now();
493                let post_process_result = candidate.holder.post_process_generation();
494                result.post_process_generation += start.elapsed();
495                let post_process_position_count = match post_process_result {
496                    Ok(count) => count,
497                    Err(error) => {
498                        Self::log_postprocessing_failure(candidate, error);
499                        continue;
500                    }
501                };
502                result.post_process_chunk_count += 1;
503                result.post_process_position_count += post_process_position_count;
504            }
505
506            if current == TickingReadiness::Unready
507                && let Err(error) = world.unpack_scheduled_ticks(candidate.pos)
508            {
509                panic!(
510                    "Full chunk scheduled-tick ownership invariant failed during readiness activation: {error:?}"
511                );
512            }
513
514            let Some(previous) = candidate
515                .holder
516                .transition_ticking_readiness(candidate.target)
517            else {
518                continue;
519            };
520            let simulation_level = candidate.holder.simulation_level();
521            result.snapshot_changed |=
522                Self::ticking_snapshot_membership(previous, simulation_level)
523                    != Self::ticking_snapshot_membership(candidate.target, simulation_level);
524            world.update_entity_chunk_visibility(
525                candidate.pos,
526                candidate.holder.entity_visibility(),
527            );
528        }
529
530        self.update_pending_readiness(&candidates);
531        result
532    }
533
534    pub(super) fn update_pending_readiness(&self, candidates: &[TickingReadinessCandidate]) {
535        let mut neighborhood = self.full_neighborhood.lock();
536        for candidate in candidates {
537            let confirmed = candidate.holder.ticking_readiness_snapshot().readiness();
538            if confirmed < candidate.desired {
539                neighborhood.ensure_pending_readiness(candidate.pos, &candidate.holder);
540            } else {
541                neighborhood.clear_pending_readiness(candidate.pos);
542            }
543        }
544    }
545
546    pub(super) fn log_postprocessing_failure(
547        candidate: &TickingReadinessCandidate,
548        error: PostProcessGenerationError,
549    ) {
550        tracing::error!(
551            chunk = ?candidate.pos,
552            ?error,
553            desired = ?candidate.desired,
554            target = ?candidate.target,
555            load_level = ?candidate.holder.load_level(),
556            "Failed to prepare Full chunk for ticking readiness"
557        );
558    }
559
560    pub(super) fn clear_all_ticking_readiness(&self) {
561        let world = self.world_gen_context.world();
562        self.chunks.iter_sync(|pos, holder| {
563            if holder
564                .transition_ticking_readiness(TickingReadiness::Unready)
565                .is_some()
566            {
567                world.update_entity_chunk_visibility(*pos, holder.entity_visibility());
568            }
569            true
570        });
571    }
572
573    pub(super) fn rebuild_ticking_readiness(
574        &self,
575    ) -> Result<ReadinessReconcileResult, FullNeighborhoodError> {
576        self.full_publications.drain();
577        let mut active = Vec::new();
578        self.chunks.iter_sync(|pos, holder| {
579            active.push((*pos, Arc::clone(holder)));
580            true
581        });
582
583        let mut rebuilt = FullNeighborhoodIndex::default();
584        for (pos, holder) in &active {
585            rebuilt.mark_dirty(*pos);
586            let contributor = if holder.load_level().is_some_and(is_full)
587                && holder.is_full_status_initialized()
588                && holder.published_status() == Some(ChunkStatus::Full)
589                && holder.try_chunk(ChunkStatus::Full).is_some()
590            {
591                Some(holder)
592            } else {
593                None
594            };
595            rebuilt.reconcile_contributor(*pos, contributor)?;
596        }
597        let dirty = rebuilt.take_dirty_counts();
598        *self.full_neighborhood.lock() = rebuilt;
599        let mut activation_holders = active.iter().map(|(_, holder)| holder).collect::<Vec<_>>();
600        activation_holders.sort_unstable_by_key(|holder| PackedChunkPos::from(holder.get_pos()));
601        self.activate_block_entities(activation_holders);
602        Ok(self.apply_final_readiness(dirty))
603    }
604
605    pub(super) fn recover_ticking_readiness_index(
606        &self,
607        error: FullNeighborhoodError,
608    ) -> ReadinessReconcileResult {
609        tracing::error!(
610            ?error,
611            "Full-neighborhood index invariant failed; rebuilding from active chunks"
612        );
613        self.clear_all_ticking_readiness();
614        *self.full_neighborhood.lock() = FullNeighborhoodIndex::default();
615        match self.rebuild_ticking_readiness() {
616            Ok(result) => result,
617            Err(rebuild_error) => {
618                tracing::error!(
619                    ?rebuild_error,
620                    "Failed to rebuild Full-neighborhood index; ticking readiness remains revoked"
621                );
622                self.clear_all_ticking_readiness();
623                *self.full_neighborhood.lock() = FullNeighborhoodIndex::default();
624                ReadinessReconcileResult::default()
625            }
626        }
627    }
628}