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