motion-core 0.1.0

Backend-independent motion engine primitives
Documentation
//! Shared LRU storage for pure authoring computations with optional byte accounting.

use std::any::{Any, TypeId};
use std::collections::{VecDeque, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
use std::panic::Location;
use std::sync::{Arc, Mutex};

use super::CacheLimits;

/// Shared LRU storage for pure authoring computations, with optional byte accounting.
///
/// Values must depend only on their keys and immutable resources. Resource
/// revisions belong in the key. The default retains 256 entries without a byte cap.
/// with_limits adds a caller-declared logical byte allowance for cache_with_cost.
/// Ordinary cache() is entry-bounded only and reports no retained-byte charge.
/// Outstanding Arc pins and allocator overhead are not bounded.
/// Concurrent misses may compute more than once; builders and cost callbacks run
/// outside the lock. Zero entries disable retention; zero bytes disable costed values.
/// Original keys are retained and compared by type and equality after hashing.
pub struct SceneCache {
    capacity: usize,
    max_bytes: usize,
    pub(super) entries: Mutex<VecDeque<CacheEntry>>,
    pub(super) empty_assets: crate::Assets,
}

impl std::fmt::Debug for SceneCache {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("SceneCache")
            .field("capacity", &self.capacity)
            .field("max_bytes", &self.max_bytes)
            .finish_non_exhaustive()
    }
}

pub(super) struct CacheEntry {
    key: CacheKey,
    input: Box<dyn Any + Send + Sync>,
    value: Arc<dyn Any + Send + Sync>,
    bytes: usize,
}

#[derive(PartialEq, Eq)]
struct CacheKey {
    location: &'static Location<'static>,
    value_type: TypeId,
    hash: u64,
}

impl CacheEntry {
    fn matches<Key: Eq + 'static>(&self, key: &CacheKey, input: &Key) -> bool {
        self.key == *key && self.input.downcast_ref::<Key>() == Some(input)
    }
}

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

impl SceneCache {
    /// Creates a cache with at most this many retained entries; zero disables it.
    pub fn new(capacity: usize) -> Self {
        Self::with_limits(CacheLimits {
            entries: capacity,
            bytes: usize::MAX,
        })
    }

    /// Bounds entries and caller-declared retained payload bytes.
    /// Heap-owning keys and values need SceneContext::cache_with_cost for payload charges.
    /// Neither allocator overhead nor externally pinned Arc values are bounded.
    pub fn with_limits(limits: CacheLimits) -> Self {
        Self {
            capacity: limits.entries,
            max_bytes: limits.bytes,
            entries: Mutex::new(VecDeque::new()),
            empty_assets: crate::Assets::default(),
        }
    }

    /// Logical payload allowance; usize::MAX means no configured byte bound.
    pub fn max_bytes(&self) -> usize {
        self.max_bytes
    }

    /// Sum of the declared charges for currently retained entries.
    pub fn retained_bytes(&self) -> usize {
        self.entries
            .lock()
            .expect("scene cache lock poisoned")
            .iter()
            .map(|entry| entry.bytes)
            .sum()
    }

    /// Maximum number of retained entries, not a byte budget.
    pub fn capacity(&self) -> usize {
        self.capacity
    }

    /// Number of currently retained entries.
    pub fn len(&self) -> usize {
        self.entries
            .lock()
            .expect("scene cache lock poisoned")
            .len()
    }

    /// Whether no computed values are currently retained.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Removes retained entries without invalidating existing returned handles.
    pub fn clear(&self) {
        let entries = std::mem::take(&mut *self.entries.lock().expect("scene cache lock poisoned"));
        drop(entries);
    }

    #[track_caller]
    pub(super) fn get<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
        &self,
        key: Key,
        build: impl FnOnce() -> Value,
    ) -> Arc<Value> {
        self.get_cached(key, build, |_| 0, false, Location::caller())
    }

    /// Gets a pure computed value with a caller-declared retained payload charge.
    #[track_caller]
    pub fn get_with_cost<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
        &self,
        key: Key,
        build: impl FnOnce() -> Value,
        cost: impl FnOnce(&Value) -> usize,
    ) -> Arc<Value> {
        self.get_cached(key, build, cost, true, Location::caller())
    }

    fn content_location() -> &'static Location<'static> {
        Location::caller()
    }

    /// Looks up a renderer content key independently of the caller's source location.
    pub fn lookup<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
        &self,
        key: &Key,
    ) -> Option<Arc<Value>> {
        let mut hasher = DefaultHasher::new();
        key.hash(&mut hasher);
        self.find(
            &CacheKey {
                location: Self::content_location(),
                value_type: TypeId::of::<Value>(),
                hash: hasher.finish(),
            },
            key,
        )
    }

    /// Inserts checked pure content, converging concurrent inserts to the retained value.
    pub fn insert_with_cost<
        Key: Eq + Hash + Send + Sync + 'static,
        Value: Send + Sync + 'static,
    >(
        &self,
        key: Key,
        value: Value,
        cost: impl FnOnce(&Value) -> usize,
    ) -> Arc<Value> {
        self.get_cached(key, || value, cost, true, Self::content_location())
    }

    #[track_caller]
    fn get_cached<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
        &self,
        key: Key,
        build: impl FnOnce() -> Value,
        cost: impl FnOnce(&Value) -> usize,
        accounted: bool,
        location: &'static Location<'static>,
    ) -> Arc<Value> {
        if self.capacity == 0 || (accounted && self.max_bytes == 0) {
            return Arc::new(build());
        }
        let mut hasher = DefaultHasher::new();
        key.hash(&mut hasher);
        let cache_key = CacheKey {
            location,
            value_type: TypeId::of::<Value>(),
            hash: hasher.finish(),
        };
        if let Some(value) = self.find(&cache_key, &key) {
            return value;
        }
        let value = Arc::new(build());
        let bytes = if accounted {
            cost(value.as_ref())
                .max(std::mem::size_of::<Value>().saturating_add(std::mem::size_of::<Key>()))
        } else {
            0
        };
        if bytes > self.max_bytes {
            return value;
        }
        let mut entries = self.entries.lock().expect("scene cache lock poisoned");
        if let Some(index) = entries
            .iter()
            .position(|entry| entry.matches(&cache_key, &key))
        {
            let entry = entries.remove(index).expect("located cache entry");
            let stored = entry.value.clone();
            entries.push_back(entry);
            drop(entries);
            return stored.downcast().unwrap_or_else(|_| unreachable!());
        }
        let mut retained: usize = entries.iter().map(|entry| entry.bytes).sum();
        let mut evicted = Vec::new();
        while entries.len() >= self.capacity || bytes > self.max_bytes.saturating_sub(retained) {
            let Some(entry) = entries.pop_front() else {
                break;
            };
            retained -= entry.bytes;
            evicted.push(entry);
        }
        entries.push_back(CacheEntry {
            key: cache_key,
            input: Box::new(key),
            value: value.clone(),
            bytes,
        });
        drop(entries);
        drop(evicted);
        value
    }

    fn find<Key: Eq + 'static, Value: Send + Sync + 'static>(
        &self,
        key: &CacheKey,
        input: &Key,
    ) -> Option<Arc<Value>> {
        let mut entries = self.entries.lock().expect("scene cache lock poisoned");
        let index = entries.iter().position(|entry| entry.matches(key, input))?;
        let entry = entries.remove(index).expect("located cache entry");
        let value = entry.value.clone();
        entries.push_back(entry);
        drop(entries);
        Some(value.downcast().unwrap_or_else(|_| unreachable!()))
    }
}