Skip to main content

concinnity_core/ecs/
join.rs

1// Index from an entity to the components it has and to its row in each
2// component's column. A multi-component query uses it to find the entities that
3// have a required set of components (and lack an excluded set), then reads each
4// component by row without scanning. It is keyed by entity index and kept in
5// sync as components are added to or removed from entities.
6//
7// The index records the full Entity occupying each slot, not just its index, so
8// a stale handle (one whose slot has since been despawned and recycled to a new
9// generation) resolves to an empty mask and no row rather than aliasing the new
10// occupant's components. A new occupant of a recycled index also self-heals:
11// the first `set` for it drops any leftover state the previous occupant left
12// behind, so a missed `clear` can never leak the previous occupant's components
13// into the new one.
14
15use alloc::vec::Vec;
16
17use crate::ecs::entity::Entity;
18use crate::ecs::mask::{ComponentId, ComponentMask};
19
20// Sentinel for "this entity has no row in this component's column".
21const NO_ROW: u32 = u32::MAX;
22
23#[derive(Default, Debug)]
24/// Entity to component-row index: which components an entity has, and
25/// where each one's row sits in its column.
26pub struct JoinIndex {
27    // entity index -> the entity currently occupying that index (None = unused).
28    // Generation-checked so a recycled index rejects the old occupant's handle.
29    occupants: Vec<Option<Entity>>,
30    // entity index -> the set of components that entity has.
31    masks: Vec<ComponentMask>,
32    // component id -> (entity index -> row in that component's column). The
33    // outer vec grows per used component id, the inner per entity index.
34    rows: Vec<Vec<u32>>,
35}
36
37impl JoinIndex {
38    /// An empty index.
39    pub fn new() -> JoinIndex {
40        JoinIndex::default()
41    }
42
43    /// Record that `entity` has component `id`, stored at `row` in that
44    /// component's column. If `entity` is a fresh occupant of its index (first
45    /// use, or a recycled index), the previous occupant's state is dropped first.
46    pub fn set(&mut self, entity: Entity, id: ComponentId, row: u32) {
47        let index = entity.index() as usize;
48        self.grow_to(index);
49        if self.occupants[index] != Some(entity) {
50            self.reset_index(index);
51            self.occupants[index] = Some(entity);
52        }
53        self.masks[index].insert(id);
54        let column = self.row_column(id.get());
55        if index >= column.len() {
56            column.resize(index + 1, NO_ROW);
57        }
58        column[index] = row;
59    }
60
61    /// Forget that `entity` has component `id`. Frees the index slot once the
62    /// entity has no components left, so the index reads as unused again.
63    pub fn clear(&mut self, entity: Entity, id: ComponentId) {
64        let index = entity.index() as usize;
65        if self.occupants.get(index).copied().flatten() != Some(entity) {
66            return;
67        }
68        self.masks[index].remove(id);
69        if let Some(slot) = self
70            .rows
71            .get_mut(id.get() as usize)
72            .and_then(|column| column.get_mut(index))
73        {
74            *slot = NO_ROW;
75        }
76        if self.masks[index].is_empty() {
77            self.occupants[index] = None;
78        }
79    }
80
81    /// Forget every component of `entity`. Called when the entity is despawned.
82    pub fn clear_entity(&mut self, entity: Entity) {
83        let index = entity.index() as usize;
84        if self.occupants.get(index).copied().flatten() != Some(entity) {
85            return;
86        }
87        self.reset_index(index);
88    }
89
90    /// The set of components `entity` has. An empty mask for a stale handle.
91    pub fn mask(&self, entity: Entity) -> ComponentMask {
92        let index = entity.index() as usize;
93        if self.occupants.get(index).copied().flatten() != Some(entity) {
94            return ComponentMask::EMPTY;
95        }
96        self.masks
97            .get(index)
98            .copied()
99            .unwrap_or(ComponentMask::EMPTY)
100    }
101
102    /// The row of `entity` in component `id`'s column, or `None` if it lacks that
103    /// component (or the handle is stale).
104    pub fn row(&self, entity: Entity, id: ComponentId) -> Option<u32> {
105        let index = entity.index() as usize;
106        if self.occupants.get(index).copied().flatten() != Some(entity) {
107            return None;
108        }
109        let row = self.rows.get(id.get() as usize)?.get(index).copied()?;
110        (row != NO_ROW).then_some(row)
111    }
112
113    /// Whether `entity` has all of `required` and none of `excluded`.
114    pub fn matches(
115        &self,
116        entity: Entity,
117        required: ComponentMask,
118        excluded: ComponentMask,
119    ) -> bool {
120        let mask = self.mask(entity);
121        mask.contains_all(required) && mask.is_disjoint(excluded)
122    }
123
124    // Drop all state recorded for an index, without touching the occupant slot.
125    fn reset_index(&mut self, index: usize) {
126        if let Some(mask) = self.masks.get_mut(index) {
127            *mask = ComponentMask::EMPTY;
128        }
129        for column in &mut self.rows {
130            if let Some(slot) = column.get_mut(index) {
131                *slot = NO_ROW;
132            }
133        }
134        if index < self.occupants.len() {
135            self.occupants[index] = None;
136        }
137    }
138
139    // Grow the per-index vectors so `index` is addressable.
140    fn grow_to(&mut self, index: usize) {
141        if index >= self.occupants.len() {
142            self.occupants.resize(index + 1, None);
143        }
144        if index >= self.masks.len() {
145            self.masks.resize(index + 1, ComponentMask::EMPTY);
146        }
147    }
148
149    fn row_column(&mut self, id: u8) -> &mut Vec<u32> {
150        let id = id as usize;
151        if id >= self.rows.len() {
152            self.rows.resize_with(id + 1, Vec::new);
153        }
154        &mut self.rows[id]
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::ecs::entity::Entities;
162
163    fn ids() -> (ComponentId, ComponentId, ComponentId) {
164        (
165            ComponentId::new(1),
166            ComponentId::new(2),
167            ComponentId::new(3),
168        )
169    }
170
171    #[test]
172    fn set_records_mask_and_row() {
173        let mut entities = Entities::new();
174        let e = entities.alloc();
175        let (transform, mesh, _) = ids();
176        let mut join = JoinIndex::new();
177        join.set(e, transform, 0);
178        join.set(e, mesh, 7);
179
180        assert!(join.mask(e).contains(transform));
181        assert!(join.mask(e).contains(mesh));
182        assert_eq!(join.row(e, transform), Some(0));
183        assert_eq!(join.row(e, mesh), Some(7));
184        // A component the entity does not have.
185        assert_eq!(join.row(e, ComponentId::new(9)), None);
186    }
187
188    #[test]
189    fn clear_removes_one_component() {
190        let mut entities = Entities::new();
191        let e = entities.alloc();
192        let (transform, mesh, _) = ids();
193        let mut join = JoinIndex::new();
194        join.set(e, transform, 0);
195        join.set(e, mesh, 1);
196
197        join.clear(e, mesh);
198        assert!(join.mask(e).contains(transform));
199        assert!(!join.mask(e).contains(mesh));
200        assert_eq!(join.row(e, mesh), None);
201        assert_eq!(join.row(e, transform), Some(0));
202    }
203
204    #[test]
205    fn clear_last_component_frees_the_slot() {
206        let mut entities = Entities::new();
207        let e = entities.alloc();
208        let (transform, _, _) = ids();
209        let mut join = JoinIndex::new();
210        join.set(e, transform, 0);
211        join.clear(e, transform);
212        // With no components left the index reads as unused.
213        assert!(join.mask(e).is_empty());
214        assert_eq!(join.row(e, transform), None);
215    }
216
217    #[test]
218    fn clear_entity_removes_everything() {
219        let mut entities = Entities::new();
220        let e = entities.alloc();
221        let (transform, mesh, collider) = ids();
222        let mut join = JoinIndex::new();
223        join.set(e, transform, 0);
224        join.set(e, mesh, 1);
225        join.set(e, collider, 2);
226
227        join.clear_entity(e);
228        assert!(join.mask(e).is_empty());
229        assert_eq!(join.row(e, transform), None);
230        assert_eq!(join.row(e, mesh), None);
231        assert_eq!(join.row(e, collider), None);
232    }
233
234    #[test]
235    fn matches_required_and_excluded() {
236        let mut entities = Entities::new();
237        let e = entities.alloc();
238        let (transform, mesh, collider) = ids();
239        let mut join = JoinIndex::new();
240        join.set(e, transform, 0);
241        join.set(e, mesh, 1);
242
243        let required = ComponentMask::with(transform);
244        let want_mesh = {
245            let mut m = ComponentMask::with(transform);
246            m.insert(mesh);
247            m
248        };
249        assert!(join.matches(e, required, ComponentMask::with(collider)));
250        assert!(join.matches(e, want_mesh, ComponentMask::EMPTY));
251        // Excluding a component it has fails the filter.
252        assert!(!join.matches(e, required, ComponentMask::with(mesh)));
253        // Requiring a component it lacks fails the filter.
254        assert!(!join.matches(e, ComponentMask::with(collider), ComponentMask::EMPTY));
255    }
256
257    #[test]
258    fn distinct_entities_are_independent() {
259        let mut entities = Entities::new();
260        let a = entities.alloc();
261        let b = entities.alloc();
262        let (transform, mesh, _) = ids();
263        let mut join = JoinIndex::new();
264        join.set(a, transform, 0);
265        join.set(b, mesh, 0);
266
267        assert!(join.mask(a).contains(transform));
268        assert!(!join.mask(a).contains(mesh));
269        assert!(join.mask(b).contains(mesh));
270        assert!(!join.mask(b).contains(transform));
271    }
272
273    #[test]
274    fn stale_handle_resolves_to_empty_not_the_recycled_occupant() {
275        let mut entities = Entities::new();
276        let a = entities.alloc();
277        let (transform, mesh, _) = ids();
278        let mut join = JoinIndex::new();
279        join.set(a, transform, 0);
280        join.set(a, mesh, 5);
281
282        // Recycle a's index without clearing the join (simulating a missed
283        // clear): the same index comes back at a new generation.
284        entities.despawn(a);
285        let b = entities.alloc();
286        assert_eq!(a.index(), b.index());
287        assert_ne!(a, b);
288
289        // The new occupant's first set self-heals the stale state.
290        join.set(b, transform, 0);
291        // The new occupant only has what it was given.
292        assert!(join.mask(b).contains(transform));
293        assert!(!join.mask(b).contains(mesh));
294        assert_eq!(join.row(b, mesh), None);
295        // The stale handle reads as empty, never aliasing b's components.
296        assert!(join.mask(a).is_empty());
297        assert_eq!(join.row(a, transform), None);
298    }
299
300    #[test]
301    fn recycled_index_rejects_old_generation_before_overwrite() {
302        let mut entities = Entities::new();
303        let a = entities.alloc();
304        let (transform, _, _) = ids();
305        let mut join = JoinIndex::new();
306        join.set(a, transform, 0);
307        // Recycle the index. Until the new occupant records anything, the join
308        // still names `a` as the occupant, but the wrong-generation handle `b`
309        // is rejected, so it can never read `a`'s components.
310        entities.despawn(a);
311        let b = entities.alloc();
312        assert_eq!(a.index(), b.index());
313        assert!(join.mask(b).is_empty());
314        assert_eq!(join.row(b, transform), None);
315    }
316}