mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Layer Styles (Drop Shadow, Inner Shadow, Outer/Inner Glow, Bevel and Emboss, Satin, Color/Gradient Overlay, Stroke) and Blending Options (Global Light, Fill Opacity, R/G/B, Knockout, Blend Interior Styles as Group) as a layerStyles property group with AE names, order and defaults. Rendered in layer space after effects (exterior passes composite with their own blend modes below the body), cached per pass, scaled with render resolution. Commands layer.style.* (menu Layer > Layer Styles), Global Light shared per comp, timeline eye switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
266 lines
8.5 KiB
Rust
266 lines
8.5 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 {
|
|
// 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()
|
|
}
|