latticeon 0.1.0

A math and ECS library focused on easy academic reproduction of animation, physics simulation, and AI
Documentation
//! Entity and entity allocation.
//!
//! Entities are opaque IDs (with generation) managed by an allocator that supports reuse and thread-safe allocation.

use std::sync::atomic::{AtomicU32, Ordering};

/// Default capacity for the entity free list when no hint is given.
const DEFAULT_ENTITY_CAPACITY: u32 = 1024;

/// An entity handle: unique id and generation to detect use-after-free.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Entity {
    pub id: u32,
    pub generation: u32,
}

impl Entity {
    #[inline]
    pub const fn new(id: u32, generation: u32) -> Self {
        Self { id, generation }
    }
}

impl std::fmt::Debug for Entity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Entity({}, gen{})", self.id, self.generation)
    }
}

/// Thread-safe entity allocator.
///
/// Allocates and recycles entity ids; each recycled id gets a new generation so stale handles fail validation.
pub struct EntityAllocator {
    /// Next id to allocate when the free list is empty.
    next_id: AtomicU32,
    /// Free list: indices that were recycled. Protected by mutex for push/pop.
    free_list: std::sync::Mutex<Vec<u32>>,
    /// For each id, current generation. Used when recycling to bump generation.
    generations: std::sync::RwLock<Vec<u32>>,
}

impl EntityAllocator {
    /// Create a new allocator with default capacity hint.
    pub fn new() -> Self {
        Self::with_capacity(DEFAULT_ENTITY_CAPACITY)
    }

    /// Create with an initial capacity for the generation array (and free list reserve).
    pub fn with_capacity(capacity: u32) -> Self {
        let mut generations = Vec::with_capacity(capacity as usize);
        generations.resize(capacity as usize, 0);
        Self {
            next_id: AtomicU32::new(0),
            free_list: std::sync::Mutex::new(Vec::new()),
            generations: std::sync::RwLock::new(generations),
        }
    }

    /// Allocate a new entity. Thread-safe.
    pub fn alloc(&self) -> Entity {
        let id = {
            let mut free = self.free_list.lock().expect("entity allocator lock");
            if let Some(id) = free.pop() {
                id
            } else {
                let id = self.next_id.fetch_add(1, Ordering::Relaxed);
                let mut gen = self.generations.write().expect("generations rwlock");
                let len = gen.len();
                if (id as usize) >= len {
                    gen.resize(len.max(1) * 2, 0);
                }
                id
            }
        };
        let generation = {
            let gen = self.generations.read().expect("generations rwlock");
            gen.get(id as usize).copied().unwrap_or(0)
        };
        Entity::new(id, generation)
    }

    /// Return an entity to the free list and bump its generation so existing handles become invalid.
    pub fn free(&self, entity: Entity) {
        let id = entity.id;
        {
            let mut gen = self.generations.write().expect("generations rwlock");
            let len = gen.len();
            if (id as usize) >= len {
                gen.resize(len.max(1) * 2, 0);
            }
            let old = gen.get(id as usize).copied().unwrap_or(0);
            gen[id as usize] = old.wrapping_add(1);
        }
        self.free_list
            .lock()
            .expect("entity allocator lock")
            .push(id);
    }

    /// Check if an entity is still valid (has not been freed and had its generation bumped).
    pub fn is_valid(&self, entity: Entity) -> bool {
        let gen = self.generations.read().expect("generations rwlock");
        gen.get(entity.id as usize)
            .copied()
            .map(|g| g == entity.generation)
            .unwrap_or(false)
    }
}

impl Default for EntityAllocator {
    fn default() -> Self {
        Self::new()
    }
}

// Safe: we use AtomicU32, Mutex, RwLock; no Rc/RefCell.
unsafe impl Send for EntityAllocator {}
unsafe impl Sync for EntityAllocator {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn alloc_and_free() {
        let alloc = EntityAllocator::new();
        let e1 = alloc.alloc();
        let e2 = alloc.alloc();
        assert_ne!(e1.id, e2.id);
        assert!(alloc.is_valid(e1));
        assert!(alloc.is_valid(e2));
        alloc.free(e1);
        assert!(!alloc.is_valid(e1));
        let e3 = alloc.alloc();
        assert_eq!(e3.id, e1.id);
        assert_ne!(e3.generation, e1.generation);
    }
}