Skip to main content

loam_runtime/
entity.rs

1use std::fmt;
2use std::sync::atomic::{AtomicU32, Ordering};
3
4use crate::command::Rejection;
5use crate::domain::DomainError;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
8pub struct RuntimeId(u32);
9
10static NEXT_RUNTIME: AtomicU32 = AtomicU32::new(1);
11
12impl RuntimeId {
13    pub(crate) fn allocate() -> Self {
14        match NEXT_RUNTIME
15            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
16        {
17            Ok(id) => Self(id),
18            Err(_) => panic!("runtime id space exhausted"),
19        }
20    }
21}
22
23/// Advances on every restore and never rewinds.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct Epoch(u32);
26
27impl Epoch {
28    pub fn advance(self) -> Self {
29        match self.0.checked_add(1) {
30            Some(epoch) => Self(epoch),
31            None => panic!("epoch space exhausted"),
32        }
33    }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
37pub struct SceneId {
38    pub(crate) runtime: RuntimeId,
39    pub(crate) epoch: Epoch,
40}
41
42impl SceneId {
43    pub(crate) const UNBOUND: Self = Self {
44        runtime: RuntimeId(0),
45        epoch: Epoch(0),
46    };
47
48    pub fn runtime(self) -> RuntimeId {
49        self.runtime
50    }
51
52    pub fn epoch(self) -> Epoch {
53        self.epoch
54    }
55}
56
57/// Slot and generation; survives a restore and never leaves the session.
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct EntityKey {
60    slot: u32,
61    generation: u32,
62}
63
64impl EntityKey {
65    pub fn slot(self) -> u32 {
66        self.slot
67    }
68
69    pub fn generation(self) -> u32 {
70        self.generation
71    }
72}
73
74impl fmt::Display for EntityKey {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "entity {} gen {}", self.slot, self.generation)
77    }
78}
79
80/// Resolves to its original object or fails: after a restore, in another session, after despawn.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
82pub struct Entity {
83    scene: SceneId,
84    key: EntityKey,
85}
86
87impl Entity {
88    pub(crate) fn new(scene: SceneId, key: EntityKey) -> Self {
89        Self { scene, key }
90    }
91
92    pub fn scene(self) -> SceneId {
93        self.scene
94    }
95
96    pub fn key(self) -> EntityKey {
97        self.key
98    }
99}
100
101impl fmt::Display for Entity {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        fmt::Display::fmt(&self.key, f)
104    }
105}
106
107#[derive(Clone)]
108struct Slot {
109    generation: u32,
110    live: bool,
111    reserved: bool,
112}
113
114pub struct Entities {
115    scene: SceneId,
116    slots: Vec<Slot>,
117    free: Vec<u32>,
118    live: usize,
119}
120
121impl Entities {
122    pub(crate) fn new(scene: SceneId) -> Self {
123        Self {
124            scene,
125            slots: Vec::new(),
126            free: Vec::new(),
127            live: 0,
128        }
129    }
130
131    pub fn scene(&self) -> SceneId {
132        self.scene
133    }
134
135    pub fn len(&self) -> usize {
136        self.live
137    }
138
139    pub fn is_empty(&self) -> bool {
140        self.live == 0
141    }
142
143    pub fn spawn(&mut self) -> Entity {
144        let entity = self.reserve();
145        self.commit(entity);
146        entity
147    }
148
149    pub(crate) fn reserve(&mut self) -> Entity {
150        let slot = match self.free.pop() {
151            Some(slot) => slot,
152            None => {
153                self.slots.push(Slot {
154                    generation: 0,
155                    live: false,
156                    reserved: false,
157                });
158                (self.slots.len() - 1) as u32
159            }
160        };
161        let entry = &mut self.slots[slot as usize];
162        entry.reserved = true;
163        Entity::new(
164            self.scene,
165            EntityKey {
166                slot,
167                generation: entry.generation,
168            },
169        )
170    }
171
172    pub(crate) fn commit(&mut self, entity: Entity) {
173        if let Some(slot) = self.reserved_index(entity) {
174            self.slots[slot].reserved = false;
175            self.slots[slot].live = true;
176            self.live += 1;
177        }
178    }
179
180    pub(crate) fn release(&mut self, entity: Entity) {
181        if let Some(slot) = self.reserved_index(entity) {
182            self.slots[slot].reserved = false;
183            self.retire(slot);
184        }
185    }
186
187    pub fn is_reserved(&self, entity: Entity) -> bool {
188        self.reserved_index(entity).is_some()
189    }
190
191    pub(crate) fn has_reservations(&self) -> bool {
192        self.slots.iter().any(|slot| slot.reserved)
193    }
194
195    fn reserved_index(&self, entity: Entity) -> Option<usize> {
196        if entity.scene != self.scene {
197            return None;
198        }
199        let key = entity.key;
200        let slot = self.slots.get(key.slot as usize)?;
201        (slot.reserved && slot.generation == key.generation).then_some(key.slot as usize)
202    }
203
204    /// A slot whose generation would wrap is retired instead of recycled.
205    pub fn despawn(&mut self, entity: Entity) -> Result<EntityKey, Rejection> {
206        let key = self
207            .resolve(entity)
208            .ok_or(Rejection::Domain(DomainError::Stale(entity)))?;
209        self.slots[key.slot as usize].live = false;
210        self.live -= 1;
211        self.retire(key.slot as usize);
212        Ok(key)
213    }
214
215    fn retire(&mut self, slot: usize) {
216        let entry = &mut self.slots[slot];
217        if let Some(next) = entry.generation.checked_add(1) {
218            entry.generation = next;
219            self.free.push(slot as u32);
220        }
221    }
222
223    pub fn resolve(&self, entity: Entity) -> Option<EntityKey> {
224        if entity.scene != self.scene {
225            return None;
226        }
227        let key = entity.key;
228        let slot = self.slots.get(key.slot as usize)?;
229        (slot.live && slot.generation == key.generation).then_some(key)
230    }
231
232    pub fn snapshot(&self) -> EntitiesSnapshot {
233        EntitiesSnapshot {
234            slots: self.slots.clone(),
235            free: self.free.clone(),
236            live: self.live,
237        }
238    }
239
240    /// Advances the epoch; every handle from before the restore fails.
241    pub fn restore(&mut self, from: &EntitiesSnapshot) {
242        self.scene.epoch = self.scene.epoch.advance();
243        self.slots.clone_from(&from.slots);
244        self.free.clone_from(&from.free);
245        self.live = from.live;
246    }
247}
248
249pub struct EntitiesSnapshot {
250    slots: Vec<Slot>,
251    free: Vec<u32>,
252    live: usize,
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn scene(runtime: u32) -> SceneId {
260        SceneId {
261            runtime: RuntimeId(runtime),
262            epoch: Epoch::default(),
263        }
264    }
265
266    #[test]
267    fn stale_reset_or_foreign_handle_never_reaches_a_recycled_object() {
268        let mut entities = Entities::new(scene(1));
269        let doomed = entities.spawn();
270        assert!(entities.despawn(doomed).is_ok());
271        let recycled = entities.spawn();
272        assert_eq!(recycled.key().slot(), doomed.key().slot());
273        assert_eq!(entities.resolve(doomed), None);
274        assert_eq!(entities.resolve(recycled), Some(recycled.key()));
275        assert!(matches!(
276            entities.despawn(doomed),
277            Err(Rejection::Domain(DomainError::Stale(stale))) if stale == doomed
278        ));
279        assert_eq!(entities.len(), 1);
280
281        let foreign = Entity::new(scene(2), recycled.key());
282        assert_eq!(entities.resolve(foreign), None);
283
284        let snapshot = entities.snapshot();
285        entities.restore(&snapshot);
286        assert_eq!(entities.resolve(recycled), None);
287        let rebased = Entity::new(entities.scene(), recycled.key());
288        assert_eq!(entities.resolve(rebased), Some(recycled.key()));
289        assert_eq!(entities.scene().epoch(), Epoch::default().advance());
290    }
291}