Skip to main content

steel_core/chunk/
full_chunk_readiness.rs

1//! Incremental Full-neighborhood tracking for chunk ticking readiness.
2
3use std::{
4    mem,
5    sync::{Arc, Weak},
6};
7
8use rustc_hash::{FxHashMap, FxHashSet};
9use steel_utils::{ChunkPos, locks::SyncMutex};
10
11use super::chunk_holder::{ChunkHolder, ChunkSaveDependency};
12
13const BLOCK_TICKING_FULL_COUNT: u8 = 9;
14const ENTITY_TICKING_FULL_COUNT: u8 = 25;
15
16/// Queue receiving notifications after a holder publishes `ChunkStatus::Full`.
17///
18/// `ChunkMap` owns the queue strongly. Holders keep only a weak sink so an
19/// unloading holder cannot retain its map.
20#[derive(Default)]
21pub(crate) struct FullPublicationQueue {
22    pending: SyncMutex<Vec<FullPublication>>,
23}
24
25impl FullPublicationQueue {
26    pub(crate) fn publish(&self, holder: &Arc<ChunkHolder>) {
27        self.pending.lock().push(FullPublication {
28            pos: holder.get_pos(),
29            holder: Arc::downgrade(holder),
30        });
31    }
32
33    pub(crate) fn drain(&self) -> Vec<FullPublication> {
34        mem::take(&mut *self.pending.lock())
35    }
36}
37
38pub(crate) struct FullPublication {
39    pub(crate) pos: ChunkPos,
40    pub(crate) holder: Weak<ChunkHolder>,
41}
42
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub(crate) struct FullNeighborhoodCounts {
45    pub(crate) block_ticking: u8,
46    pub(crate) entity_ticking: u8,
47}
48
49impl FullNeighborhoodCounts {
50    #[must_use]
51    pub(crate) const fn block_ticking_ready(self) -> bool {
52        self.block_ticking == BLOCK_TICKING_FULL_COUNT
53    }
54
55    #[must_use]
56    pub(crate) const fn entity_ticking_ready(self) -> bool {
57        self.entity_ticking == ENTITY_TICKING_FULL_COUNT
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(crate) enum FullNeighborhoodError {
63    CounterOverflow {
64        center: ChunkPos,
65        radius: u8,
66        count: u8,
67    },
68    CounterUnderflow {
69        center: ChunkPos,
70        radius: u8,
71    },
72    ContributorIdentityMismatch {
73        pos: ChunkPos,
74    },
75}
76
77struct PendingReadiness {
78    holder: Weak<ChunkHolder>,
79    _save_dependency: ChunkSaveDependency,
80}
81
82#[derive(Default)]
83pub(crate) struct FullNeighborhoodIndex {
84    contributors: FxHashMap<ChunkPos, Weak<ChunkHolder>>,
85    counts: FxHashMap<ChunkPos, FullNeighborhoodCounts>,
86    dirty_centers: FxHashSet<ChunkPos>,
87    pending_readiness: FxHashMap<ChunkPos, PendingReadiness>,
88}
89
90impl FullNeighborhoodIndex {
91    pub(crate) fn reconcile_contributor(
92        &mut self,
93        pos: ChunkPos,
94        holder: Option<&Arc<ChunkHolder>>,
95    ) -> Result<(), FullNeighborhoodError> {
96        let unchanged = match (self.contributors.get(&pos), holder) {
97            (Some(current), Some(holder)) => current.ptr_eq(&Arc::downgrade(holder)),
98            (None, None) => true,
99            (Some(_), None) | (None, Some(_)) => false,
100        };
101        if unchanged {
102            return Ok(());
103        }
104
105        if self.contributors.contains_key(&pos) {
106            self.remove_contributor(pos)?;
107        }
108        if let Some(holder) = holder {
109            self.add_contributor(pos, Arc::downgrade(holder))?;
110        }
111        Ok(())
112    }
113
114    pub(crate) fn remove_contributor_if_matches(
115        &mut self,
116        pos: ChunkPos,
117        holder: &Arc<ChunkHolder>,
118    ) -> Result<(), FullNeighborhoodError> {
119        let expected = Arc::downgrade(holder);
120        match self.contributors.get(&pos) {
121            None => Ok(()),
122            Some(current) if current.ptr_eq(&expected) => self.remove_contributor(pos),
123            Some(_) => Err(FullNeighborhoodError::ContributorIdentityMismatch { pos }),
124        }
125    }
126
127    fn add_contributor(
128        &mut self,
129        pos: ChunkPos,
130        holder: Weak<ChunkHolder>,
131    ) -> Result<(), FullNeighborhoodError> {
132        debug_assert!(!self.contributors.contains_key(&pos));
133        self.validate_increment(pos, 1, BLOCK_TICKING_FULL_COUNT)?;
134        self.validate_increment(pos, 2, ENTITY_TICKING_FULL_COUNT)?;
135
136        self.contributors.insert(pos, holder);
137        self.adjust_counts(pos, 1, true);
138        self.adjust_counts(pos, 2, true);
139        Ok(())
140    }
141
142    fn remove_contributor(&mut self, pos: ChunkPos) -> Result<(), FullNeighborhoodError> {
143        debug_assert!(self.contributors.contains_key(&pos));
144        self.validate_decrement(pos, 1)?;
145        self.validate_decrement(pos, 2)?;
146
147        self.contributors.remove(&pos);
148        self.adjust_counts(pos, 1, false);
149        self.adjust_counts(pos, 2, false);
150        Self::for_each_center(pos, 2, |center| {
151            if self
152                .counts
153                .get(&center)
154                .is_some_and(|counts| counts.block_ticking == 0 && counts.entity_ticking == 0)
155            {
156                self.counts.remove(&center);
157            }
158        });
159        Ok(())
160    }
161
162    fn validate_increment(
163        &self,
164        pos: ChunkPos,
165        radius: u8,
166        maximum: u8,
167    ) -> Result<(), FullNeighborhoodError> {
168        let mut result = Ok(());
169        Self::for_each_center(pos, radius, |center| {
170            if result.is_err() {
171                return;
172            }
173            let counts = self.counts.get(&center).copied().unwrap_or_default();
174            let count = if radius == 1 {
175                counts.block_ticking
176            } else {
177                counts.entity_ticking
178            };
179            if count >= maximum {
180                result = Err(FullNeighborhoodError::CounterOverflow {
181                    center,
182                    radius,
183                    count,
184                });
185            }
186        });
187        result
188    }
189
190    fn validate_decrement(&self, pos: ChunkPos, radius: u8) -> Result<(), FullNeighborhoodError> {
191        let mut result = Ok(());
192        Self::for_each_center(pos, radius, |center| {
193            if result.is_err() {
194                return;
195            }
196            let counts = self.counts.get(&center).copied().unwrap_or_default();
197            let count = if radius == 1 {
198                counts.block_ticking
199            } else {
200                counts.entity_ticking
201            };
202            if count == 0 {
203                result = Err(FullNeighborhoodError::CounterUnderflow { center, radius });
204            }
205        });
206        result
207    }
208
209    fn adjust_counts(&mut self, pos: ChunkPos, radius: u8, increment: bool) {
210        Self::for_each_center(pos, radius, |center| {
211            let counts = self.counts.entry(center).or_default();
212            let count = if radius == 1 {
213                &mut counts.block_ticking
214            } else {
215                &mut counts.entity_ticking
216            };
217            if increment {
218                *count += 1;
219            } else {
220                *count -= 1;
221            }
222            self.dirty_centers.insert(center);
223        });
224    }
225
226    fn for_each_center(pos: ChunkPos, radius: u8, mut f: impl FnMut(ChunkPos)) {
227        let radius = i32::from(radius);
228        for dz in -radius..=radius {
229            for dx in -radius..=radius {
230                let Some(x) = pos.0.x.checked_add(dx) else {
231                    continue;
232                };
233                let Some(z) = pos.0.y.checked_add(dz) else {
234                    continue;
235                };
236                f(ChunkPos::new(x, z));
237            }
238        }
239    }
240
241    pub(crate) fn mark_dirty(&mut self, pos: ChunkPos) {
242        self.dirty_centers.insert(pos);
243    }
244
245    pub(crate) fn dirty_counts_snapshot(&self) -> Vec<(ChunkPos, FullNeighborhoodCounts)> {
246        self.dirty_centers
247            .iter()
248            .copied()
249            .map(|pos| (pos, self.counts.get(&pos).copied().unwrap_or_default()))
250            .collect()
251    }
252
253    pub(crate) fn take_dirty_counts(&mut self) -> Vec<(ChunkPos, FullNeighborhoodCounts)> {
254        self.dirty_centers
255            .drain()
256            .map(|pos| (pos, self.counts.get(&pos).copied().unwrap_or_default()))
257            .collect()
258    }
259
260    pub(crate) fn ensure_pending_readiness(&mut self, pos: ChunkPos, holder: &Arc<ChunkHolder>) {
261        let holder_weak = Arc::downgrade(holder);
262        if self
263            .pending_readiness
264            .get(&pos)
265            .is_some_and(|pending| pending.holder.ptr_eq(&holder_weak))
266        {
267            return;
268        }
269
270        self.pending_readiness.insert(
271            pos,
272            PendingReadiness {
273                holder: holder_weak,
274                _save_dependency: holder.add_save_dependency(),
275            },
276        );
277    }
278
279    pub(crate) fn clear_pending_readiness(&mut self, pos: ChunkPos) {
280        self.pending_readiness.remove(&pos);
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn full_square_counts_track_block_and_entity_neighborhoods() {
290        let mut index = FullNeighborhoodIndex::default();
291        for z in -2..=2 {
292            for x in -2..=2 {
293                index
294                    .add_contributor(ChunkPos::new(x, z), Weak::new())
295                    .expect("unique contributors stay within neighborhood bounds");
296            }
297        }
298
299        assert_eq!(
300            index.counts.get(&ChunkPos::new(0, 0)),
301            Some(&FullNeighborhoodCounts {
302                block_ticking: BLOCK_TICKING_FULL_COUNT,
303                entity_ticking: ENTITY_TICKING_FULL_COUNT,
304            })
305        );
306
307        index
308            .remove_contributor(ChunkPos::new(-2, -2))
309            .expect("existing contributor has matching counters");
310        assert_eq!(
311            index.counts.get(&ChunkPos::new(0, 0)),
312            Some(&FullNeighborhoodCounts {
313                block_ticking: BLOCK_TICKING_FULL_COUNT,
314                entity_ticking: ENTITY_TICKING_FULL_COUNT - 1,
315            })
316        );
317    }
318
319    #[test]
320    fn invalid_extreme_coordinates_do_not_overflow() {
321        let mut index = FullNeighborhoodIndex::default();
322        let pos = ChunkPos::new(i32::MAX, i32::MIN);
323
324        index
325            .add_contributor(pos, Weak::new())
326            .expect("representable neighboring centers should be counted");
327        index
328            .remove_contributor(pos)
329            .expect("the same representable centers should be decremented");
330
331        assert!(index.counts.is_empty());
332    }
333}