ezu_graph/cache.rs
1//! Render-time intermediate cache, keyed by a content-derived hash.
2//!
3//! A bounded LRU keeps long editor sessions from growing without limit.
4//! Default capacity is 4096 entries; tune via [`Cache::with_capacity`].
5//! At ~1.3 MB per padded raster that ceilings around ~5 GB worst case,
6//! but in practice intermediates are mostly small (masks, features) and
7//! the cap-by-count is the right knob.
8
9use std::num::NonZeroUsize;
10use std::sync::Mutex;
11
12use lru::LruCache;
13use xxhash_rust::xxh3::Xxh3;
14
15use crate::eval::{CanvasInfo, TileId};
16use crate::value::PortValue;
17
18/// 128-bit content hash. Wide enough that collisions are not a concern
19/// for our scale; narrow enough to fit four words.
20pub type Hash128 = u128;
21
22/// Compose a cache key for one node evaluation.
23///
24/// The key folds together:
25/// - the canvas (tile_size + pad), so cached buffers always match shape
26/// - the tile id (or omitted for world-anchored nodes)
27/// - the node's own param hash
28/// - each input's cache hash (Merkle-style chain)
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct CacheKey(pub Hash128);
31
32impl CacheKey {
33 pub fn build(
34 canvas: CanvasInfo,
35 tile: Option<TileId>,
36 params_hash: Hash128,
37 inputs: &[Hash128],
38 ) -> Self {
39 let mut h = Xxh3::new();
40 h.update(&canvas.tile_size.to_le_bytes());
41 h.update(&canvas.pad.to_le_bytes());
42 if let Some(t) = tile {
43 h.update(&[t.z]);
44 h.update(&t.x.to_le_bytes());
45 h.update(&t.y.to_le_bytes());
46 }
47 h.update(¶ms_hash.to_le_bytes());
48 for i in inputs {
49 h.update(&i.to_le_bytes());
50 }
51 CacheKey(h.digest128())
52 }
53}
54
55/// Default LRU capacity. Each entry holds an `Arc<PortValue>` so the
56/// payload is shared, not duplicated; the cap bounds how many distinct
57/// intermediates the evaluator remembers, not raw bytes.
58pub const DEFAULT_CAPACITY: usize = 4096;
59
60/// Shared cache of evaluated `PortValue`s. Cloning a `PortValue` is
61/// cheap (Arc-backed for the heavy variants) so cache reuse adds
62/// near-zero overhead.
63pub struct Cache {
64 inner: Mutex<LruCache<CacheKey, PortValue>>,
65}
66
67impl Default for Cache {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Cache {
74 pub fn new() -> Self {
75 Self::with_capacity(DEFAULT_CAPACITY)
76 }
77
78 pub fn with_capacity(cap: usize) -> Self {
79 // `cap.max(1)` guarantees the value is non-zero.
80 let cap = NonZeroUsize::new(cap.max(1)).expect("cap.max(1) is non-zero");
81 Self {
82 inner: Mutex::new(LruCache::new(cap)),
83 }
84 }
85
86 /// Look up a cached value and refresh its LRU position.
87 pub fn get(&self, key: CacheKey) -> Option<PortValue> {
88 self.lock().get(&key).cloned()
89 }
90
91 pub fn insert(&self, key: CacheKey, value: PortValue) {
92 self.lock().put(key, value);
93 }
94
95 pub fn len(&self) -> usize {
96 self.lock().len()
97 }
98
99 pub fn is_empty(&self) -> bool {
100 self.lock().is_empty()
101 }
102
103 pub fn clear(&self) {
104 self.lock().clear();
105 }
106
107 /// Configured maximum entry count.
108 pub fn capacity(&self) -> usize {
109 self.lock().cap().get()
110 }
111
112 /// Acquire the inner mutex. Recovers from poisoning by taking the
113 /// guard anyway — the cache holds no invariant that a panic mid-op
114 /// could break (it's just an LRU of `Arc`s).
115 fn lock(&self) -> std::sync::MutexGuard<'_, LruCache<CacheKey, PortValue>> {
116 self.inner.lock().unwrap_or_else(|e| e.into_inner())
117 }
118}