Skip to main content

motion_core/authoring/context/
cache.rs

1//! Shared LRU storage for pure authoring computations with optional byte accounting.
2
3use std::any::{Any, TypeId};
4use std::collections::{VecDeque, hash_map::DefaultHasher};
5use std::hash::{Hash, Hasher};
6use std::panic::Location;
7use std::sync::{Arc, Mutex};
8
9use super::CacheLimits;
10
11/// Shared LRU storage for pure authoring computations, with optional byte accounting.
12///
13/// Values must depend only on their keys and immutable resources. Resource
14/// revisions belong in the key. The default retains 256 entries without a byte cap.
15/// with_limits adds a caller-declared logical byte allowance for cache_with_cost.
16/// Ordinary cache() is entry-bounded only and reports no retained-byte charge.
17/// Outstanding Arc pins and allocator overhead are not bounded.
18/// Concurrent misses may compute more than once; builders and cost callbacks run
19/// outside the lock. Zero entries disable retention; zero bytes disable costed values.
20/// Original keys are retained and compared by type and equality after hashing.
21pub struct SceneCache {
22    capacity: usize,
23    max_bytes: usize,
24    pub(super) entries: Mutex<VecDeque<CacheEntry>>,
25    pub(super) empty_assets: crate::Assets,
26}
27
28impl std::fmt::Debug for SceneCache {
29    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        formatter
31            .debug_struct("SceneCache")
32            .field("capacity", &self.capacity)
33            .field("max_bytes", &self.max_bytes)
34            .finish_non_exhaustive()
35    }
36}
37
38pub(super) struct CacheEntry {
39    key: CacheKey,
40    input: Box<dyn Any + Send + Sync>,
41    value: Arc<dyn Any + Send + Sync>,
42    bytes: usize,
43}
44
45#[derive(PartialEq, Eq)]
46struct CacheKey {
47    location: &'static Location<'static>,
48    value_type: TypeId,
49    hash: u64,
50}
51
52impl CacheEntry {
53    fn matches<Key: Eq + 'static>(&self, key: &CacheKey, input: &Key) -> bool {
54        self.key == *key && self.input.downcast_ref::<Key>() == Some(input)
55    }
56}
57
58impl Default for SceneCache {
59    fn default() -> Self {
60        Self::new(256)
61    }
62}
63
64impl SceneCache {
65    /// Creates a cache with at most this many retained entries; zero disables it.
66    pub fn new(capacity: usize) -> Self {
67        Self::with_limits(CacheLimits {
68            entries: capacity,
69            bytes: usize::MAX,
70        })
71    }
72
73    /// Bounds entries and caller-declared retained payload bytes.
74    /// Heap-owning keys and values need SceneContext::cache_with_cost for payload charges.
75    /// Neither allocator overhead nor externally pinned Arc values are bounded.
76    pub fn with_limits(limits: CacheLimits) -> Self {
77        Self {
78            capacity: limits.entries,
79            max_bytes: limits.bytes,
80            entries: Mutex::new(VecDeque::new()),
81            empty_assets: crate::Assets::default(),
82        }
83    }
84
85    /// Logical payload allowance; usize::MAX means no configured byte bound.
86    pub fn max_bytes(&self) -> usize {
87        self.max_bytes
88    }
89
90    /// Sum of the declared charges for currently retained entries.
91    pub fn retained_bytes(&self) -> usize {
92        self.entries
93            .lock()
94            .expect("scene cache lock poisoned")
95            .iter()
96            .map(|entry| entry.bytes)
97            .sum()
98    }
99
100    /// Maximum number of retained entries, not a byte budget.
101    pub fn capacity(&self) -> usize {
102        self.capacity
103    }
104
105    /// Number of currently retained entries.
106    pub fn len(&self) -> usize {
107        self.entries
108            .lock()
109            .expect("scene cache lock poisoned")
110            .len()
111    }
112
113    /// Whether no computed values are currently retained.
114    pub fn is_empty(&self) -> bool {
115        self.len() == 0
116    }
117
118    /// Removes retained entries without invalidating existing returned handles.
119    pub fn clear(&self) {
120        let entries = std::mem::take(&mut *self.entries.lock().expect("scene cache lock poisoned"));
121        drop(entries);
122    }
123
124    #[track_caller]
125    pub(super) fn get<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
126        &self,
127        key: Key,
128        build: impl FnOnce() -> Value,
129    ) -> Arc<Value> {
130        self.get_cached(key, build, |_| 0, false, Location::caller())
131    }
132
133    /// Gets a pure computed value with a caller-declared retained payload charge.
134    #[track_caller]
135    pub fn get_with_cost<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
136        &self,
137        key: Key,
138        build: impl FnOnce() -> Value,
139        cost: impl FnOnce(&Value) -> usize,
140    ) -> Arc<Value> {
141        self.get_cached(key, build, cost, true, Location::caller())
142    }
143
144    fn content_location() -> &'static Location<'static> {
145        Location::caller()
146    }
147
148    /// Looks up a renderer content key independently of the caller's source location.
149    pub fn lookup<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
150        &self,
151        key: &Key,
152    ) -> Option<Arc<Value>> {
153        let mut hasher = DefaultHasher::new();
154        key.hash(&mut hasher);
155        self.find(
156            &CacheKey {
157                location: Self::content_location(),
158                value_type: TypeId::of::<Value>(),
159                hash: hasher.finish(),
160            },
161            key,
162        )
163    }
164
165    /// Inserts checked pure content, converging concurrent inserts to the retained value.
166    pub fn insert_with_cost<
167        Key: Eq + Hash + Send + Sync + 'static,
168        Value: Send + Sync + 'static,
169    >(
170        &self,
171        key: Key,
172        value: Value,
173        cost: impl FnOnce(&Value) -> usize,
174    ) -> Arc<Value> {
175        self.get_cached(key, || value, cost, true, Self::content_location())
176    }
177
178    #[track_caller]
179    fn get_cached<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
180        &self,
181        key: Key,
182        build: impl FnOnce() -> Value,
183        cost: impl FnOnce(&Value) -> usize,
184        accounted: bool,
185        location: &'static Location<'static>,
186    ) -> Arc<Value> {
187        if self.capacity == 0 || (accounted && self.max_bytes == 0) {
188            return Arc::new(build());
189        }
190        let mut hasher = DefaultHasher::new();
191        key.hash(&mut hasher);
192        let cache_key = CacheKey {
193            location,
194            value_type: TypeId::of::<Value>(),
195            hash: hasher.finish(),
196        };
197        if let Some(value) = self.find(&cache_key, &key) {
198            return value;
199        }
200        let value = Arc::new(build());
201        let bytes = if accounted {
202            cost(value.as_ref())
203                .max(std::mem::size_of::<Value>().saturating_add(std::mem::size_of::<Key>()))
204        } else {
205            0
206        };
207        if bytes > self.max_bytes {
208            return value;
209        }
210        let mut entries = self.entries.lock().expect("scene cache lock poisoned");
211        if let Some(index) = entries
212            .iter()
213            .position(|entry| entry.matches(&cache_key, &key))
214        {
215            let entry = entries.remove(index).expect("located cache entry");
216            let stored = entry.value.clone();
217            entries.push_back(entry);
218            drop(entries);
219            return stored.downcast().unwrap_or_else(|_| unreachable!());
220        }
221        let mut retained: usize = entries.iter().map(|entry| entry.bytes).sum();
222        let mut evicted = Vec::new();
223        while entries.len() >= self.capacity || bytes > self.max_bytes.saturating_sub(retained) {
224            let Some(entry) = entries.pop_front() else {
225                break;
226            };
227            retained -= entry.bytes;
228            evicted.push(entry);
229        }
230        entries.push_back(CacheEntry {
231            key: cache_key,
232            input: Box::new(key),
233            value: value.clone(),
234            bytes,
235        });
236        drop(entries);
237        drop(evicted);
238        value
239    }
240
241    fn find<Key: Eq + 'static, Value: Send + Sync + 'static>(
242        &self,
243        key: &CacheKey,
244        input: &Key,
245    ) -> Option<Arc<Value>> {
246        let mut entries = self.entries.lock().expect("scene cache lock poisoned");
247        let index = entries.iter().position(|entry| entry.matches(key, input))?;
248        let entry = entries.remove(index).expect("located cache entry");
249        let value = entry.value.clone();
250        entries.push_back(entry);
251        drop(entries);
252        Some(value.downcast().unwrap_or_else(|_| unreachable!()))
253    }
254}