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 #[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 #[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); 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 #[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 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 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 chunk_holder.wake_all_watchers();
180
181 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 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 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 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 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}