Skip to main content

renew_ecs/
store.rs

1//! A sparse set: one component type, stored densely, addressed by entity.
2//!
3//! Three arrays. `sparse` maps an entity slot to a dense position, `dense`
4//! maps back, and `values` sits alongside `dense`. Insert and remove are
5//! constant time; removal swaps the last element into the hole, which is
6//! what keeps `values` contiguous for iteration.
7//!
8//! **That swap is why iteration order is not free**, and why this file has
9//! two iterators rather than one. After any churn the dense array is in
10//! no useful order at all, so a query that walked it would visit entities
11//! in an order decided by their removal history — reproducible only if
12//! every prior operation was. The engine defines an order instead —
13//! ascending slot — and [`Store::iter`] provides it by walking `sparse`.
14
15/// Components of one type, addressed by entity slot.
16#[derive(Debug)]
17pub struct Store<T> {
18    /// Entity slot to dense position. `u32::MAX` means absent, which
19    /// costs four bytes per slot rather than the eight an `Option<u32>`
20    /// would take at this alignment.
21    sparse: Vec<u32>,
22    dense: Vec<u32>,
23    values: Vec<T>,
24}
25
26/// The sentinel for "this slot holds nothing".
27const ABSENT: u32 = u32::MAX;
28
29impl<T> Default for Store<T> {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl<T> Store<T> {
36    /// An empty store.
37    #[must_use]
38    pub const fn new() -> Self {
39        Self {
40            sparse: Vec::new(),
41            dense: Vec::new(),
42            values: Vec::new(),
43        }
44    }
45
46    /// How many components are stored.
47    #[must_use]
48    pub fn len(&self) -> usize {
49        self.values.len()
50    }
51
52    /// Whether the store holds nothing.
53    #[must_use]
54    pub fn is_empty(&self) -> bool {
55        self.values.is_empty()
56    }
57
58    /// Whether this entity slot has a component here.
59    #[must_use]
60    pub fn contains(&self, slot: u32) -> bool {
61        self.position(slot).is_some()
62    }
63
64    /// The dense position for a slot, if any.
65    fn position(&self, slot: u32) -> Option<usize> {
66        match self.sparse.get(slot as usize).copied() {
67            Some(ABSENT) | None => None,
68            Some(position) => Some(position as usize),
69        }
70    }
71
72    /// The component for this slot.
73    #[must_use]
74    pub fn get(&self, slot: u32) -> Option<&T> {
75        self.values.get(self.position(slot)?)
76    }
77
78    /// The component for this slot, mutably.
79    pub fn get_mut(&mut self, slot: u32) -> Option<&mut T> {
80        let position = self.position(slot)?;
81        self.values.get_mut(position)
82    }
83
84    /// Store a component, returning the one it replaced.
85    pub fn insert(&mut self, slot: u32, value: T) -> Option<T> {
86        if let Some(position) = self.position(slot) {
87            let existing = self.values.get_mut(position)?;
88            return Some(core::mem::replace(existing, value));
89        }
90        let needed = (slot as usize).checked_add(1)?;
91        if self.sparse.len() < needed {
92            self.sparse.resize(needed, ABSENT);
93        }
94        let position = u32::try_from(self.dense.len()).ok()?;
95        if let Some(entry) = self.sparse.get_mut(slot as usize) {
96            *entry = position;
97        }
98        self.dense.push(slot);
99        self.values.push(value);
100        None
101    }
102
103    /// Remove a component, returning it.
104    ///
105    /// The last element is swapped into the hole, so this is constant
106    /// time and `values` stays contiguous — at the cost of `dense` losing
107    /// any order it had, which is the trade [`Store::iter`] pays for.
108    pub fn remove(&mut self, slot: u32) -> Option<T> {
109        let position = self.position(slot)?;
110        let last = self.dense.len().checked_sub(1)?;
111        self.dense.swap(position, last);
112        self.values.swap(position, last);
113
114        // The element now at `position` used to be last; point its slot
115        // at its new home. Done before the pop so a store of one element
116        // reads its own slot rather than a stale one.
117        if let Some(moved) = self.dense.get(position).copied()
118            && position != last
119            && let Some(entry) = self.sparse.get_mut(moved as usize)
120        {
121            *entry = u32::try_from(position).unwrap_or(ABSENT);
122        }
123        if let Some(entry) = self.sparse.get_mut(slot as usize) {
124            *entry = ABSENT;
125        }
126        self.dense.pop();
127        self.values.pop()
128    }
129
130    /// Every component, in ascending entity-slot order.
131    ///
132    /// **This is the order the engine promises**, and it is why the store
133    /// exists in this shape. It walks `sparse`, so its cost is
134    /// proportional to the highest occupied slot rather than to the number
135    /// of components — a store scattered across a wide slot range pays for
136    /// the gaps. That is the measured cost of a defined order, and the
137    /// entity allocator reuses low slots first precisely to keep it small.
138    pub fn iter(&self) -> impl Iterator<Item = (u32, &T)> + '_ {
139        self.sparse
140            .iter()
141            .enumerate()
142            .filter(|(_, position)| **position != ABSENT)
143            .filter_map(move |(slot, position)| {
144                let value = self.values.get(*position as usize)?;
145                Some((u32::try_from(slot).ok()?, value))
146            })
147    }
148
149    /// How far [`Store::iter`] has to walk: slots scanned, not components
150    /// found.
151    ///
152    /// `sparse` grows to the highest slot ever inserted and never shrinks,
153    /// so this is a high-water mark rather than a live measurement. It is
154    /// the cost model, which is why it is crate-private: the only caller
155    /// that should care is [`crate::join`], choosing which side to walk.
156    /// Exposing it publicly would invite callers to branch on a number
157    /// that says nothing about the data they are about to see.
158    pub(crate) fn scan_len(&self) -> usize {
159        self.sparse.len()
160    }
161
162    /// Every component in slot order, mutably.
163    ///
164    /// Collects the visit order first, because handing out `&mut` while
165    /// borrowing `sparse` to decide the order is not something the borrow
166    /// checker will allow — and the honest fix is one allocation per
167    /// call, not `unsafe`.
168    pub fn for_each_mut(&mut self, mut visit: impl FnMut(u32, &mut T)) {
169        let order: Vec<(u32, u32)> = self
170            .sparse
171            .iter()
172            .enumerate()
173            .filter(|(_, position)| **position != ABSENT)
174            .filter_map(|(slot, position)| Some((u32::try_from(slot).ok()?, *position)))
175            .collect();
176        for (slot, position) in order {
177            if let Some(value) = self.values.get_mut(position as usize) {
178                visit(slot, value);
179            }
180        }
181    }
182
183    /// The components in storage order, which is **unspecified**.
184    ///
185    /// Offered because it is what a system that does not care about order
186    /// should use: it is a flat walk of a contiguous array, with none of
187    /// the gap-skipping [`Store::iter`] pays for. Any system whose result
188    /// depends on the order it sees is wrong to use this, and the name is
189    /// the warning.
190    pub fn iter_unordered(&self) -> impl Iterator<Item = &T> + '_ {
191        self.values.iter()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn a_new_store_is_empty() {
201        let store: Store<u32> = Store::new();
202        assert!(store.is_empty());
203        assert_eq!(store.len(), 0);
204        assert!(store.get(0).is_none());
205        assert!(!store.contains(0));
206    }
207
208    /// `Default` is what a struct holding a store will use, so it has to
209    /// agree with `new` rather than merely compile.
210    #[test]
211    fn default_and_new_agree() {
212        let made: Store<u32> = Store::default();
213        assert!(made.is_empty());
214        assert_eq!(made.len(), Store::<u32>::new().len());
215    }
216
217    #[test]
218    fn insert_then_get_returns_the_value() {
219        let mut store = Store::new();
220        assert!(store.insert(3, "three").is_none());
221        assert_eq!(store.get(3), Some(&"three"));
222        assert!(store.contains(3));
223        assert_eq!(store.len(), 1);
224        // A slot below the inserted one exists in `sparse` and is empty.
225        assert!(!store.contains(0));
226    }
227
228    #[test]
229    fn inserting_twice_replaces_and_returns_the_old_value() {
230        let mut store = Store::new();
231        store.insert(1, 10);
232        assert_eq!(store.insert(1, 20), Some(10));
233        assert_eq!(store.get(1), Some(&20));
234        assert_eq!(store.len(), 1, "replacing must not grow the store");
235    }
236
237    #[test]
238    fn get_mut_edits_in_place() {
239        let mut store = Store::new();
240        store.insert(2, 5);
241        if let Some(value) = store.get_mut(2) {
242            *value += 1;
243        }
244        assert_eq!(store.get(2), Some(&6));
245        assert!(store.get_mut(9).is_none());
246    }
247
248    /// The swap-remove has to fix up the moved element's back-pointer.
249    /// Getting this wrong is the classic sparse-set bug and it only shows
250    /// when the removed element is not the last one.
251    #[test]
252    fn removing_from_the_middle_keeps_every_other_lookup_correct() {
253        let mut store = Store::new();
254        for slot in 0..5 {
255            store.insert(slot, slot * 100);
256        }
257        assert_eq!(store.remove(1), Some(100));
258        assert_eq!(store.len(), 4);
259        assert!(!store.contains(1));
260        for slot in [0u32, 2, 3, 4] {
261            assert_eq!(store.get(slot), Some(&(slot * 100)), "slot {slot}");
262        }
263    }
264
265    #[test]
266    fn removing_the_last_element_is_also_correct() {
267        let mut store = Store::new();
268        store.insert(0, 'a');
269        store.insert(1, 'b');
270        assert_eq!(store.remove(1), Some('b'));
271        assert_eq!(store.get(0), Some(&'a'));
272        assert!(!store.contains(1));
273        assert_eq!(store.remove(0), Some('a'));
274        assert!(store.is_empty());
275    }
276
277    #[test]
278    fn removing_what_is_not_there_returns_nothing() {
279        let mut store: Store<u8> = Store::new();
280        assert!(store.remove(7).is_none());
281        store.insert(0, 1);
282        assert!(store.remove(7).is_none());
283        assert_eq!(store.len(), 1);
284    }
285
286    /// The contract: iteration is by ascending slot however the store was
287    /// churned. Built by inserting out of order and removing from the
288    /// middle, which is exactly what leaves `dense` scrambled.
289    #[test]
290    fn iteration_is_by_slot_whatever_the_churn() {
291        let mut store = Store::new();
292        for slot in [5u32, 1, 9, 3, 7] {
293            store.insert(slot, slot);
294        }
295        store.remove(3);
296        store.insert(2, 2);
297        store.remove(9);
298
299        let seen: Vec<u32> = store.iter().map(|(slot, _)| slot).collect();
300        assert_eq!(seen, vec![1, 2, 5, 7]);
301
302        // And the dense order is genuinely different, or the test above
303        // would prove nothing.
304        let dense: Vec<u32> = store.iter_unordered().copied().collect();
305        assert_ne!(
306            dense, seen,
307            "dense order happened to match; pick harsher churn"
308        );
309    }
310
311    #[test]
312    fn for_each_mut_visits_in_slot_order_and_can_edit() {
313        let mut store = Store::new();
314        for slot in [4u32, 0, 2] {
315            store.insert(slot, slot);
316        }
317        let mut order = Vec::new();
318        store.for_each_mut(|slot, value| {
319            order.push(slot);
320            *value += 1;
321        });
322        assert_eq!(order, vec![0, 2, 4]);
323        assert_eq!(store.get(0), Some(&1));
324        assert_eq!(store.get(4), Some(&5));
325    }
326
327    #[test]
328    fn the_store_survives_a_long_churn() {
329        let mut store = Store::new();
330        for round in 0..50u32 {
331            for slot in 0..20u32 {
332                store.insert(slot, round * 100 + slot);
333            }
334            for slot in (0..20u32).step_by(3) {
335                store.remove(slot);
336            }
337        }
338        let seen: Vec<u32> = store.iter().map(|(slot, _)| slot).collect();
339        let expected: Vec<u32> = (0..20).filter(|slot| slot % 3 != 0).collect();
340        assert_eq!(seen, expected);
341        assert_eq!(store.len(), expected.len());
342    }
343}