//! Layer render cache. //! //! The processed pixels of a layer (source → masks → effects, in layer space) depend only on the //! layer's own non-transform properties, its source item and the render options — not on its //! transform, opacity, blend mode or anything below it. [`LayerCache`] keeps those buffers keyed //! by a content hash of exactly those inputs, *evaluated at the frame time*, so: //! //! * a static layer (or one whose only animation is in Transform) renders once and is reused on //! every frame while scrubbing or playing; //! * editing one layer re-renders only that layer — every other layer's cached pixels stay valid //! because their keys do not change; //! * any change to a property value, keyframe, expression result, effect, mask, switch or source //! item changes the key, so stale pixels are never returned. //! //! Layers whose pixels depend on time in ways the property values do not capture (time-based //! effects such as Noise or Wave Warp, Wiggle Paths, footage, precomps) either fold the layer //! time into the key or are not cached. use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use effectcraft_effects::Buf; use effectcraft_project::{ItemKind, Layer, LayerSource, Node, PropGroup}; use crate::eval::EvalCtx; /// Thread-safe, memory-budgeted cache of processed layer buffers. pub struct LayerCache { inner: Mutex, } struct Entry { buf: Arc, bytes: usize, last_use: u64, } struct Inner { map: HashMap, bytes: usize, budget: usize, clock: u64, hits: u64, misses: u64, } /// Hit/miss counters and memory use (for perf readouts and tests). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct CacheStats { pub entries: usize, pub bytes: usize, pub hits: u64, pub misses: u64, } impl Default for LayerCache { fn default() -> Self { LayerCache::new(1 << 30) } } impl LayerCache { /// A cache holding at most `budget` bytes of pixels. pub fn new(budget: usize) -> LayerCache { LayerCache { inner: Mutex::new(Inner { map: HashMap::new(), bytes: 0, budget, clock: 0, hits: 0, misses: 0 }) } } pub fn get(&self, key: u64) -> Option> { let mut g = self.inner.lock().ok()?; g.clock += 1; let now = g.clock; match g.map.get_mut(&key) { Some(e) => { e.last_use = now; let b = e.buf.clone(); g.hits += 1; Some(b) } None => { g.misses += 1; None } } } pub fn insert(&self, key: u64, buf: Arc) { let bytes = buf.img.data.len() * std::mem::size_of::(); let Ok(mut g) = self.inner.lock() else { return }; if bytes > g.budget / 4 { return; } g.clock += 1; let now = g.clock; if let Some(old) = g.map.insert(key, Entry { buf, bytes, last_use: now }) { g.bytes -= old.bytes; } g.bytes += bytes; if g.bytes > g.budget { // Evict least-recently used entries down to 3/4 of the budget. let target = g.budget / 4 * 3; let mut by_age: Vec<(u64, u64)> = g.map.iter().map(|(k, e)| (e.last_use, *k)).collect(); by_age.sort_unstable(); for (_, k) in by_age { if g.bytes <= target { break; } if let Some(e) = g.map.remove(&k) { g.bytes -= e.bytes; } } } } pub fn clear(&self) { if let Ok(mut g) = self.inner.lock() { g.map.clear(); g.bytes = 0; } } pub fn stats(&self) -> CacheStats { self.inner.lock().map(|g| CacheStats { entries: g.map.len(), bytes: g.bytes, hits: g.hits, misses: g.misses }).unwrap_or_default() } } /// FNV-1a style 64-bit hasher fed by `Debug` output (exact float formatting) and raw values. struct KeyHasher(u64); impl Hasher for KeyHasher { fn finish(&self) -> u64 { self.0 } fn write(&mut self, bytes: &[u8]) { for &b in bytes { self.0 ^= b as u64; self.0 = self.0.wrapping_mul(0x0100_0000_01b3); } } } impl std::fmt::Write for KeyHasher { fn write_str(&mut self, s: &str) -> std::fmt::Result { Hasher::write(self, s.as_bytes()); Ok(()) } } fn hash_debug(h: &mut KeyHasher, v: &impl std::fmt::Debug) { use std::fmt::Write; let _ = write!(h, "{v:?}"); Hasher::write_u8(h, 0xff); } /// Time-based layer content: does any part of the source/effects read the clock directly /// (rather than through property values)? fn time_dependent(layer: &Layer) -> bool { if layer.switches.effects && let Some(fx) = layer.effects() { for g in fx.groups() { if let effectcraft_project::GroupKind::Effect { effect } = &g.kind && g.enabled && effectcraft_effects::is_time_dependent(effect) { return true; } } } if matches!(layer.source, LayerSource::Shape) && let Some(c) = layer.props.sub("contents") { return group_has(c, "wiggle"); } false } fn group_has(g: &PropGroup, match_id: &str) -> bool { g.children.iter().any(|c| match c { Node::Group(sub) => sub.match_id == match_id || group_has(sub, match_id), Node::Prop(_) => false, }) } /// Feed every property of `g` that can change over time (keyframes or expression) as its value /// at the context time. The static structure is hashed separately. fn hash_values(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, g: &PropGroup) { for c in &g.children { match c { Node::Prop(p) => { if !p.keys.is_empty() || p.has_expression() { p.uid.hash(h); hash_debug(h, &ctx.value(layer, p)); } } Node::Group(sub) => hash_values(h, ctx, layer, sub), } } } /// Cache key for the processed (source → masks → effects) buffer of `layer` at the context /// time, or `None` when the layer must not be cached (footage, precomps, cameras, adjustment /// layers…). pub fn layer_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool) -> Option { if layer.switches.adjustment { return None; } let mut h = KeyHasher(0xcbf2_9ce4_8422_2325); match &layer.source { LayerSource::Solid { item } => { let it = ctx.project.item(*item)?; let ItemKind::Solid(s) = &it.kind else { return None }; hash_debug(&mut h, s); } LayerSource::Text | LayerSource::Shape => {} _ => return None, } ctx.comp_id.hash(&mut h); ctx.comp.width.hash(&mut h); ctx.comp.height.hash(&mut h); scale.to_bits().hash(&mut h); draft.hash(&mut h); layer.id.hash(&mut h); hash_debug(&mut h, &layer.source); hash_debug(&mut h, &layer.switches); for c in &layer.props.children { // Transform is applied later; Layer Styles are keyed separately (see [`styles_key`]). if let Node::Group(g) = c && (g.match_id == "transform" || g.match_id == effectcraft_project::styles::GROUP) { continue; } // Static structure: values, keyframes, expressions, enabled flags, effect ids, modes. hash_debug(&mut h, c); if let Node::Group(g) = c { hash_values(&mut h, ctx, layer, g); } } if time_dependent(layer) { layer.layer_time(ctx.time).0.hash(&mut h); // Effects also see the layer's time mapping. layer.start_time.0.hash(&mut h); layer.stretch.to_bits().hash(&mut h); } Some(h.finish()) } /// Cache key for the styled layer (content key + the Layer Styles group: structure, eye /// switches, values, keyframes, expressions and their values at the context time — Global Light /// included, as every layer mirrors it in its Blending Options). pub fn styles_key(ctx: &EvalCtx, layer: &Layer, content_key: u64) -> u64 { let mut h = KeyHasher(content_key ^ 0x9e37_79b9_7f4a_7c15); if let Some(g) = layer.layer_styles() { hash_debug(&mut h, g); hash_values(&mut h, ctx, layer, g); } h.finish() } /// A sub-key of `key` (styled layer passes, flattened buffer). pub fn derive(key: u64, i: u64) -> u64 { let mut h = KeyHasher(key); i.hash(&mut h); h.finish() }