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