Skip to main content

steel_core/chunk/chunk_map/
generation_readiness.rs

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