Skip to main content

concinnity_core/ecs/
entity.rs

1// Generational entity handle and the allocator that hands them out. An Entity
2// is a runtime-only identity for one live instance; it is never serialized. The
3// generation makes a handle to a despawned-and-recycled slot detectable, so a
4// stale handle resolves to a safe `None` rather than aliasing a new entity.
5//
6// The allocator supports two ways to mint ids: `alloc` under `&mut self`
7// (recycles freed slots), and `reserve` under `&self` (lock-free, fresh ids
8// only) for command recording on worker threads. `flush` materializes reserved
9// ids into the metadata table before they are looked up.
10
11use alloc::vec::Vec;
12
13use core::num::NonZeroU32;
14use core::sync::atomic::{AtomicU32, Ordering};
15
16// Generation 1 is the first valid generation; the NonZeroU32 niche keeps a
17// zeroed handle invalid and Option<Entity> at 8 bytes.
18const FIRST_GEN: NonZeroU32 = NonZeroU32::MIN;
19
20#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
21/// A live entity handle: a slot index plus the generation that slot
22/// carried when the handle was minted.
23pub struct Entity {
24    index: u32,
25    generation: NonZeroU32,
26}
27
28impl Entity {
29    pub(crate) fn new(index: u32, generation: NonZeroU32) -> Entity {
30        Entity { index, generation }
31    }
32
33    /// The entity's slot index.
34    pub fn index(self) -> u32 {
35        self.index
36    }
37
38    /// The generation the slot carried when this handle was minted.
39    pub fn generation(self) -> u32 {
40        self.generation.get()
41    }
42
43    /// Pack into a single u64 (index in the high half, generation in the low
44    /// half). Stable representation for an FFI / scripting boundary.
45    pub fn to_bits(self) -> u64 {
46        ((self.index as u64) << 32) | self.generation.get() as u64
47    }
48
49    /// Inverse of `to_bits`. Returns `None` when the generation half is zero,
50    /// which no live handle ever has, so a zeroed or truncated value is rejected
51    /// instead of forged into a valid-looking entity.
52    pub fn from_bits(bits: u64) -> Option<Entity> {
53        let generation = NonZeroU32::new(bits as u32)?;
54        Some(Entity {
55            index: (bits >> 32) as u32,
56            generation,
57        })
58    }
59
60    /// A handle the allocator never hands out (the top index is unreachable in
61    /// practice). For a runtime-only component that has to name an Entity field
62    /// but is never built from serialized args -- its real value is inserted at
63    /// runtime, so this placeholder is never observed by any system.
64    pub fn dangling() -> Entity {
65        Entity {
66            index: u32::MAX,
67            generation: FIRST_GEN,
68        }
69    }
70}
71
72#[derive(Debug)]
73struct EntityMeta {
74    generation: NonZeroU32,
75    alive: bool,
76}
77
78#[derive(Debug, Default)]
79/// The entity allocator: slot generations, the free list, and the
80/// reservation counter.
81pub struct Entities {
82    meta: Vec<EntityMeta>,
83    // Recycled indices, available to `alloc`. Reservation never recycles.
84    free: Vec<u32>,
85    // Next fresh index. Always >= meta.len(); the gap is reserved-but-not-yet
86    // materialized fresh ids, realized by `flush`. An atomic so `reserve` can
87    // run under `&self` from worker threads.
88    next_fresh: AtomicU32,
89}
90
91impl Entities {
92    /// An allocator with no slots.
93    pub fn new() -> Entities {
94        Entities::default()
95    }
96
97    /// Allocate an entity, recycling a freed slot when one is available. The
98    /// recycled slot keeps the generation it was bumped to at despawn, so old
99    /// handles to it stay invalid.
100    pub fn alloc(&mut self) -> Entity {
101        if let Some(index) = self.free.pop() {
102            let meta = &mut self.meta[index as usize];
103            meta.alive = true;
104            return Entity::new(index, meta.generation);
105        }
106        let index = {
107            let next = self.next_fresh.get_mut();
108            let i = *next;
109            *next += 1;
110            i
111        };
112        self.grow_to(index);
113        let meta = &mut self.meta[index as usize];
114        meta.alive = true;
115        Entity::new(index, meta.generation)
116    }
117
118    /// Reserve a fresh entity id without taking `&mut self`. Lock-free, so it is
119    /// safe to call from worker threads while recording commands. Reserved ids
120    /// are always fresh (never recycled) and carry the first generation; call
121    /// `flush` under `&mut self` before looking them up.
122    pub fn reserve(&self) -> Entity {
123        let index = self.next_fresh.fetch_add(1, Ordering::Relaxed);
124        Entity::new(index, FIRST_GEN)
125    }
126
127    /// Materialize any reserved-but-unmaterialized ids into the metadata table,
128    /// marking them alive. Idempotent.
129    pub fn flush(&mut self) {
130        let high = *self.next_fresh.get_mut();
131        if high == 0 {
132            return;
133        }
134        self.grow_to(high - 1);
135    }
136
137    /// Despawn an entity. Validates the generation, so a stale or already-dead
138    /// handle is a no-op returning `false`. Bumps the slot's generation and
139    /// frees the index for reuse.
140    pub fn despawn(&mut self, entity: Entity) -> bool {
141        self.flush();
142        let Some(meta) = self.meta.get_mut(entity.index as usize) else {
143            return false;
144        };
145        if !meta.alive || meta.generation != entity.generation {
146            return false;
147        }
148        meta.alive = false;
149        meta.generation = next_generation(meta.generation);
150        self.free.push(entity.index);
151        true
152    }
153
154    /// Whether the handle refers to a currently-live entity. Reflects only
155    /// materialized state; reserved ids appear after `flush`.
156    pub fn is_alive(&self, entity: Entity) -> bool {
157        self.meta
158            .get(entity.index as usize)
159            .is_some_and(|meta| meta.alive && meta.generation == entity.generation)
160    }
161
162    // Number of metadata slots ever allocated (live plus recycled). Not the
163    // live count.
164    #[cfg(test)]
165    pub(crate) fn total_slots(&self) -> usize {
166        self.meta.len()
167    }
168
169    // Grow the metadata table so `index` is in range. Every index below
170    // `next_fresh` was handed out by `alloc` or `reserve` and is live until
171    // despawned (the free-list tracks the despawned ones), so a newly
172    // materialized slot starts alive at the first generation.
173    fn grow_to(&mut self, index: u32) {
174        while self.meta.len() as u32 <= index {
175            self.meta.push(EntityMeta {
176                generation: FIRST_GEN,
177                alive: true,
178            });
179        }
180    }
181}
182
183// Bump a generation, wrapping past u32::MAX back to the first valid generation
184// (skipping 0, which the NonZeroU32 niche forbids). After 2^32 reuses of one
185// slot a stale handle can alias a live entity again (generational ABA).
186fn next_generation(generation: NonZeroU32) -> NonZeroU32 {
187    NonZeroU32::new(generation.get().wrapping_add(1)).unwrap_or(FIRST_GEN)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn alloc_hands_out_distinct_fresh_ids() {
196        let mut entities = Entities::new();
197        let a = entities.alloc();
198        let b = entities.alloc();
199        assert_ne!(a.index(), b.index());
200        assert_eq!(a.generation(), 1);
201        assert_eq!(b.generation(), 1);
202        assert!(entities.is_alive(a));
203        assert!(entities.is_alive(b));
204    }
205
206    #[test]
207    fn despawn_recycles_index_and_bumps_generation() {
208        let mut entities = Entities::new();
209        let a = entities.alloc();
210        assert!(entities.despawn(a));
211        // Index reused, generation advanced.
212        let b = entities.alloc();
213        assert_eq!(a.index(), b.index());
214        assert_eq!(b.generation(), 2);
215        // The stale handle no longer resolves.
216        assert!(!entities.is_alive(a));
217        assert!(entities.is_alive(b));
218    }
219
220    #[test]
221    fn double_despawn_and_stale_despawn_are_noops() {
222        let mut entities = Entities::new();
223        let a = entities.alloc();
224        assert!(entities.despawn(a));
225        assert!(!entities.despawn(a));
226        let b = entities.alloc();
227        // Despawning with the old generation must not touch the live entity.
228        assert!(!entities.despawn(a));
229        assert!(entities.is_alive(b));
230    }
231
232    #[test]
233    fn reserve_then_flush_materializes_live_entities() {
234        let mut entities = Entities::new();
235        let a = entities.reserve();
236        let b = entities.reserve();
237        assert_ne!(a.index(), b.index());
238        entities.flush();
239        assert!(entities.is_alive(a));
240        assert!(entities.is_alive(b));
241        assert_eq!(entities.total_slots(), 2);
242    }
243
244    #[test]
245    fn reserve_is_distinct_across_threads() {
246        use std::sync::Arc;
247        let entities = Arc::new(Entities::new());
248        let mut handles = Vec::new();
249        for _ in 0..8 {
250            let shared = Arc::clone(&entities);
251            handles.push(std::thread::spawn(move || {
252                (0..1000)
253                    .map(|_| shared.reserve().index())
254                    .collect::<Vec<_>>()
255            }));
256        }
257        let mut all: Vec<u32> = handles
258            .into_iter()
259            .flat_map(|h| h.join().unwrap())
260            .collect();
261        all.sort_unstable();
262        let count = all.len();
263        all.dedup();
264        assert_eq!(all.len(), count, "reserved indices must be unique");
265    }
266
267    #[test]
268    fn alloc_and_reserve_never_collide() {
269        let mut entities = Entities::new();
270        let reserved = entities.reserve();
271        let allocated = entities.alloc();
272        assert_ne!(reserved.index(), allocated.index());
273    }
274
275    #[test]
276    fn to_bits_round_trips_and_rejects_zero_generation() {
277        let mut entities = Entities::new();
278        let a = entities.alloc();
279        let bits = a.to_bits();
280        assert_eq!(Entity::from_bits(bits), Some(a));
281        // A zero generation half is never a live handle.
282        assert_eq!(Entity::from_bits(0), None);
283        assert_eq!(Entity::from_bits(7u64 << 32), None);
284    }
285
286    #[test]
287    fn generation_wraps_past_max_to_one() {
288        let max = NonZeroU32::new(u32::MAX).unwrap();
289        assert_eq!(next_generation(max), FIRST_GEN);
290        assert_eq!(next_generation(FIRST_GEN).get(), 2);
291    }
292}