246 lines
7.6 KiB
Rust
246 lines
7.6 KiB
Rust
//! 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<Inner>,
|
|
}
|
|
|
|
struct Entry {
|
|
buf: Arc<Buf>,
|
|
bytes: usize,
|
|
last_use: u64,
|
|
}
|
|
|
|
struct Inner {
|
|
map: HashMap<u64, Entry>,
|
|
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<Arc<Buf>> {
|
|
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<Buf>) {
|
|
let bytes = buf.img.data.len() * std::mem::size_of::<effectcraft_raster::Px>();
|
|
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<u64> {
|
|
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 {
|
|
if let Node::Group(g) = c
|
|
&& g.match_id == "transform"
|
|
{
|
|
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())
|
|
}
|