mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 11:37:02 -06:00
G5: still footage layers are kept in the layer cache across times (#1812)
Footage layers had no layer cache key, so a still made a new layer buffer every frame on every preview worker, and the GPU compositor uploaded it again each time (its upload cache is keyed by buffer). Stills (without a proxy) are now keyed like text and shape layers, without the source time; movies and sequences are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use effectcraft_effects::Buf;
|
||||
use effectcraft_project::{ItemKind, Layer, LayerSource, Node, PropGroup};
|
||||
use effectcraft_project::{FootageKind, ItemKind, Layer, LayerSource, Node, PropGroup};
|
||||
|
||||
use crate::eval::EvalCtx;
|
||||
|
||||
@@ -481,7 +481,7 @@ fn hash_values(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, g: &PropGroup) {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// time, or `None` when the layer must not be cached (movie footage, precomps, cameras, adjustment
|
||||
/// layers…).
|
||||
pub fn layer_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool) -> Option<u64> {
|
||||
// Time effects see property values at other times. Keyframes are part of the hashed
|
||||
@@ -514,6 +514,13 @@ fn reads_other_times(layer: &Layer) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// A still image (no proxy): its layer is cached like a text or shape layer, so the GPU
|
||||
/// compositor uploads it once, not every frame (#1812: four 21120 px plates re-uploaded by every
|
||||
/// preview worker ran the GPU out of memory). Movies and sequences stay in the media pool only.
|
||||
fn still_footage(ctx: &EvalCtx, item: effectcraft_project::ItemId) -> bool {
|
||||
ctx.project.item(item).is_some_and(|it| it.proxy.is_none() && matches!(&it.kind, ItemKind::Footage(f) if f.kind == FootageKind::Still))
|
||||
}
|
||||
|
||||
fn has_expression(g: &PropGroup) -> bool {
|
||||
g.children.iter().any(|c| match c {
|
||||
Node::Prop(p) => p.has_expression(),
|
||||
@@ -552,14 +559,17 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
hash_debug(&mut h, s);
|
||||
}
|
||||
LayerSource::Text | LayerSource::Shape => {}
|
||||
LayerSource::Footage { item } if footage => {
|
||||
LayerSource::Footage { item } if footage || still_footage(ctx, *item) => {
|
||||
let it = ctx.project.item(*item)?;
|
||||
let ItemKind::Footage(f) = &it.kind else { return None };
|
||||
hash_debug(&mut h, f);
|
||||
// A proxy (and its Use Proxy switch) changes the pixels.
|
||||
hash_debug(&mut h, &it.proxy);
|
||||
item.hash(&mut h);
|
||||
ctx.source_time(layer).0.hash(&mut h);
|
||||
// A still is the same at every time (unless a proxy, which may be a movie, stands in).
|
||||
if f.kind != FootageKind::Still || it.proxy.is_some() {
|
||||
ctx.source_time(layer).0.hash(&mut h);
|
||||
}
|
||||
// Frame blending mixes neighbouring source frames.
|
||||
ctx.comp.enable_frame_blending.hash(&mut h);
|
||||
}
|
||||
|
||||
@@ -81,3 +81,28 @@ fn reduced_renders_take_footage_resampled_by_the_source() {
|
||||
Renderer::new(&p, &sized, RenderOpts { scale: 0.5, ..Default::default() }).comp_frame(cid, Tick::ZERO);
|
||||
assert!(sized.sizes.unwrap().into_inner().unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// #1812: a still's layer is kept in the layer cache across times (its texture then uploads to
|
||||
/// the GPU once, not every frame); a movie's is fetched again at every time.
|
||||
#[test]
|
||||
fn still_layers_are_cached_across_times_and_movies_are_not() {
|
||||
for (kind, fetches) in [(FootageKind::Still, 1), (FootageKind::Video, 3)] {
|
||||
let (mut p, cid) = project(BitDepth::Bpc8, false);
|
||||
let Some(LayerSource::Footage { item }) = p.comp(cid).map(|c| c.layers[0].source.clone()) else { panic!("footage layer") };
|
||||
if let Some(ItemKind::Footage(f)) = p.item_mut(item).map(|it| &mut it.kind) {
|
||||
f.kind = kind;
|
||||
}
|
||||
let cache = crate::LayerCache::default();
|
||||
let src = Clip { sizes: Some(Mutex::default()) };
|
||||
let opts = RenderOpts { scale: 0.5, ..Default::default() };
|
||||
for t in [0.0, 0.25, 0.5] {
|
||||
let t = Tick::from_seconds_f64(t);
|
||||
let mut r = Renderer::new(&p, &src, opts);
|
||||
r.cache = Some(&cache);
|
||||
let frame = r.comp_frame(cid, t);
|
||||
assert_eq!(frame, Renderer::new(&p, &src, opts).comp_frame(cid, t), "{kind:?} at {t:?}");
|
||||
}
|
||||
// (each uncached render above fetches once more)
|
||||
assert_eq!(src.sizes.unwrap().into_inner().unwrap().len(), fetches + 3, "{kind:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user