diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index 784a393d..f7111cd4 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -857,3 +857,44 @@ fn readback_staging_stays_bounded_across_sizes() { let held = g.ctx.staging_bytes(); assert!(held <= (256 << 20) + 2 * 1024 * 16 * 376, "staging buffers hold {held} bytes"); } + +/// A static adjustment layer with a time-dependent effect (Noise) keeps its footprint key +/// across frames (the footprint is the source through the masks, not the effects), so the GPU +/// uploads nothing after the first frame; matches the CPU. +#[test] +fn adjustment_footprint_uploads_once_under_time_dependent_effects() { + let mut s = Scene::new(BitDepth::Bpc8); + let bg = s.solid([0.3, 0.5, 0.7], 96, 64); + s.push(bg); + let mut adj = s.solid([1.0, 1.0, 1.0], 80, 50); + adj.switches.adjustment = true; + s.effect(&mut adj, "ec.noise.noise", &[("amount", n(30.0))]); + s.push(adj); + // The key itself (no GPU needed): stable across time. + let comp = s.p.comp(s.cid).expect("the scene's comp"); + let adj = comp.layers.last().expect("the adjustment layer"); + let key = |k: f64| { + effectcraft_render::cache::footprint_key(&effectcraft_render::EvalCtx::new(&s.p, s.cid, comp, Tick::from_seconds_f64(k)), adj, 1.0, false, false) + }; + assert!(key(0.0).is_some()); + assert_eq!(key(0.0), key(0.5), "the footprint key changes with time"); + let Some(g) = gpu() else { return }; + let cache = effectcraft_render::cache::LayerCache::new(64 << 20); + let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() }); + r.accel = Some(g); + r.cache = Some(&cache); + let t = |k: u64| Tick::from_seconds_f64(k as f64 / 30.0); + let mut prev = None; + for k in 0..4 { + let before = g.ctx.transfer_stats().upload_bytes; + let img = g.render(&r, s.cid, t(k)).expect("the GPU renders the frame"); + let up = g.ctx.transfer_stats().upload_bytes - before; + if k >= 2 { + assert_eq!(up, 0, "frame {k} uploaded {up} bytes: the footprint was re-keyed"); + } + // Noise changes every frame: the frames differ. + assert!(prev.as_ref() != Some(&img.data), "frame {k} repeats the previous one"); + prev = Some(img.data); + check(&format!("noise adjustment frame {k}"), compare_at(&s, opts(), t(k)), 0.0); + } +} diff --git a/crates/render/src/cache.rs b/crates/render/src/cache.rs index 88fa5c7c..bae12d1e 100644 --- a/crates/render/src/cache.rs +++ b/crates/render/src/cache.rs @@ -423,8 +423,10 @@ fn hash_debug(h: &mut KeyHasher, v: &impl std::fmt::Debug) { /// 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 +/// `effects: false` ignores the effects (a key of the source and masks only). +fn time_dependent(layer: &Layer, effects: bool) -> bool { + if effects + && layer.switches.effects && let Some(fx) = layer.effects() { for g in fx.groups() { @@ -522,7 +524,10 @@ fn has_expression(g: &PropGroup) -> bool { /// Cache key for an adjustment layer's footprint (its source through its masks, see /// `Renderer::adjustment_footprint`): the GPU compositor then uploads it once, not every frame. pub fn footprint_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool) -> Option { - Some(derive(key_any(ctx, layer, scale, draft, blur, false, None)?, 0xf007_9417)) + // The footprint is the source through the masks, without the effects: a time-dependent + // effect (Noise on a finishing adjustment layer) must not re-key it every frame (on the GPU, + // a full-frame re-upload per frame). + Some(derive(key_any(ctx, layer, scale, draft, blur, false, Some(0))?, 0xf007_9417)) } fn key_with(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool) -> Option { @@ -620,7 +625,7 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo effectcraft_effects::paint::cache_key(g, lt).0.hash(&mut h); } } - if time_dependent(layer) { + if time_dependent(layer, effects != Some(0)) { layer.layer_time(ctx.time).0.hash(&mut h); // Effects also see the layer's time mapping. layer.start_time.0.hash(&mut h); diff --git a/crates/render/src/cache/dependencies.rs b/crates/render/src/cache/dependencies.rs index 8ddee7a9..eaccd058 100644 --- a/crates/render/src/cache/dependencies.rs +++ b/crates/render/src/cache/dependencies.rs @@ -78,7 +78,7 @@ impl Dependencies<'_> { hash_debug(self.h, layer); hash_values(self.h, ctx, layer, &layer.props); hash_debug(self.h, &ctx.world_matrix(layer)); - if super::time_dependent(layer) { + if super::time_dependent(layer, true) { layer.layer_time(ctx.time).0.hash(self.h); } match layer.source {