From e0bb54e4c619470e3a93534fdaf69654b4761549 Mon Sep 17 00:00:00 2001 From: DWAA1660 Date: Tue, 13 Oct 2026 18:05:48 -0600 Subject: [PATCH] render: hash the comp scene of selected layers that read it into the owner's cache key (#1252) A layer selected by another layer's effect (Displacement Map, Blend...) had its structure, values, matrix and time hashed into the owner's key, but not the comp camera/lights its own scene-reading effects (Caustics, CC Particle World...) see. The owner's key included the scene only when the owner itself ran such an effect, and a cached owner never runs the selected layer's effects, so moving the camera or a light reused stale owner pixels. Dependencies::layer now hashes comp_scene for every selected (or nested) layer that reads it, with the same policy as owner keys. Fixes #1252 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh --- crates/render/src/cache/dependencies.rs | 5 ++++ crates/render/src/tests.rs | 39 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/crates/render/src/cache/dependencies.rs b/crates/render/src/cache/dependencies.rs index eaccd058..483b19da 100644 --- a/crates/render/src/cache/dependencies.rs +++ b/crates/render/src/cache/dependencies.rs @@ -78,6 +78,11 @@ impl Dependencies<'_> { hash_debug(self.h, layer); hash_values(self.h, ctx, layer, &layer.props); hash_debug(self.h, &ctx.world_matrix(layer)); + // A cached owner never runs this layer's effects, so the comp camera and lights its + // scene-reading effects see belong in the owner's key too (#1252). + if super::reads_comp_scene(layer) { + hash_debug(self.h, &crate::comp_scene(ctx, layer)); + } if super::time_dependent(layer, true) { layer.layer_time(ctx.time).0.hash(self.h); } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 39e022b0..78577fa9 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -953,3 +953,42 @@ fn shape_content_bounds_are_the_visual_edges() { assert!(b.iter().zip([-half, -half, half, half]).all(|(a, e)| (a - e).abs() < 1e-6), "{stroke:?}: {b:?}"); } } + +/// #1252: a layer blended with a CC Particle World layer, under a camera panned by `pan` px. +fn scene_reader_dependency(pan: f64) -> (Project, ItemId) { + let (mut p, cid, comp) = setup(); + let mut source = solid(&mut p, &comp, [0.0, 0.0, 0.0], 200, 100); + let vals = [("birthRate", Value::Scalar(2.0)), ("particle/particleType", Value::Enum(2)), ("particle/birthSize", Value::Scalar(0.1))]; + add_effect(&mut p, &mut source, "ec.sim.ccparticleworld", [200.0, 100.0], &vals); + source.switches.video = false; + let mut owner = solid(&mut p, &comp, [0.0, 0.0, 0.0], 200, 100); + add_effect_200(&mut p, &mut owner, "ec.channel.blend", &[("blendWithLayer", Value::Layer(Some(source.id.0))), ("blendWithOriginal", Value::Scalar(0.0))]); + let mut cam = build::layer(&mut p, &comp, "Camera", LayerSource::Camera, (200, 100), None); + for path in ["transform/position", "transform/poi"] { + let pr = cam.props.prop_mut(path).unwrap(); + let Value::Vec3(v) = pr.value else { panic!("{path}: {:?}", pr.value) }; + pr.value = Value::Vec3([v[0] + pan, v[1], v[2]]); + } + p.comp_mut(cid).unwrap().layers = vec![cam, owner, source]; + (p, cid) +} + +/// #1252: the owner's cache key covers the comp camera and lights that a layer it selects reads, +/// though the owner itself runs no scene-reading effect. +#[test] +fn a_selected_layer_reading_the_comp_scene_invalidates_its_owner() { + let t = Tick::from_seconds_f64(1.0); + let (p, cid) = scene_reader_dependency(0.0); + let (q, _) = scene_reader_dependency(40.0); + let key = |p: &Project| { + let c = p.comp(cid).unwrap(); + crate::cache::layer_key(&crate::EvalCtx::new(p, cid, c, t), &c.layers[1], 1.0, false, false).unwrap() + }; + assert_ne!(key(&p), key(&q), "moving the camera changes the owner's key"); + let cache = crate::LayerCache::default(); + let here = render_cached(&p, cid, t, Some(&cache)); + let panned = render_cached(&q, cid, t, Some(&cache)); + assert_same(&panned, &render_cached(&q, cid, t, None), "cached render after the camera moved"); + let (a, b) = (particles_x(&here), particles_x(&panned)); + assert!(b < a - 20.0, "the blended particles follow the camera: {a} -> {b}"); +}