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