Render: an adjustment layer's footprint key ignores its effects, so a time-dependent effect (Noise) no longer re-keys it and the GPU re-uploads it every frame (#687)

This commit is contained in:
Matej Kozlovský
2026-10-12 09:47:28 +02:00
parent 24f98a0242
commit eec81bbe0e
3 changed files with 51 additions and 5 deletions
+41
View File
@@ -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);
}
}
+9 -4
View File
@@ -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<u64> {
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<u64> {
@@ -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);
+1 -1
View File
@@ -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 {