From 68d928aff325c9af3682c840b4ef7e14a015b97a Mon Sep 17 00:00:00 2001 From: arnav-hiwarkar Date: Wed, 14 Oct 2026 13:42:59 +0530 Subject: [PATCH] fix(render): preserve preceding effects in GPU Echo (#1120) --- crates/gpu/src/tests_fx_noise.rs | 22 ++++++++++++++++++++++ crates/render/src/lib.rs | 26 +++++++++++++++++--------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/gpu/src/tests_fx_noise.rs b/crates/gpu/src/tests_fx_noise.rs index 0fc384425..476f08642 100644 --- a/crates/gpu/src/tests_fx_noise.rs +++ b/crates/gpu/src/tests_fx_noise.rs @@ -203,6 +203,28 @@ fn echo_and_posterize_time() { } } +#[test] +fn echo_after_star_burst_preserves_the_stars() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for operator in 0..7 { + for posterize in [false, true] { + let mut s = Scene::new(depth); + let mut l = s.solid([1.0; 3], 97, 61); + s.effect(&mut l, "ec.sim.ccstarburst", &[("gridSpacing", n(8.0)), ("size", n(40.0))]); + s.effect(&mut l, "ec.time.echo", &[("numberOfEchoes", n(2.0)), ("echoTime", n(-0.1)), ("decay", n(0.6)), ("echoOperator", e(operator))]); + if posterize { + s.effect(&mut l, "ec.time.posterizetime", &[("frameRate", n(4.0))]); + } + s.push(l); + let t = Tick::from_seconds_f64(0.6); + let label = format!("Star Burst + Echo {operator} posterize={posterize} {depth:?}"); + check(&label, compare_at(&s, opts(), t), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0); + } + } + } +} + #[test] fn median_levels_match_cpu_division() { let Some(g) = crate::tests::gpu() else { return }; diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 41036fe6b..808356f12 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -216,7 +216,7 @@ struct FxHost<'r, 'a, 'c> { /// whose effects run on the comp below). origin: [f64; 2], /// Index of the effect being rendered (bounds `self_at` to effects before it). - index: std::sync::atomic::AtomicUsize, + index: usize, original: Option<&'c Buf>, } @@ -307,7 +307,7 @@ impl EffectHost for FxHost<'_, '_, '_> { if self.r.depth > MAX_FX_DEPTH || self.layer.switches.adjustment { return None; } - let n = effects.min(self.index.load(std::sync::atomic::Ordering::Relaxed)); + let n = effects.min(self.index); let t = self.layer.comp_time(Tick::from_seconds_f64(layer_time)); let sub = self.r.nested(); sub.layer_input(&self.ctx.at(t), self.layer, n).map(|b| (*b).clone()) @@ -337,8 +337,7 @@ impl EffectHost for FxHost<'_, '_, '_> { } fn params_at(&self, layer_time: f64) -> Option { - let i = self.index.load(std::sync::atomic::Ordering::Relaxed); - let g = self.layer.effects()?.groups().nth(i)?; + let g = self.layer.effects()?.groups().nth(self.index)?; let ctx = self.ctx.at(self.layer.comp_time(Tick::from_seconds_f64(layer_time))); Some(effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(self.layer, pr))) } @@ -1158,7 +1157,7 @@ impl<'a> Renderer<'a> { buf }); } - let host = FxHost { r: self, ctx, layer, origin, index: Default::default(), original: original.as_ref() }; + let host = FxHost { r: self, ctx, layer, origin, index: 0, original: original.as_ref() }; let env = EffectEnv { masks: &mask_shapes, host: Some(&host), @@ -1192,12 +1191,21 @@ impl<'a> Renderer<'a> { { let run: Vec<_> = stack[k..].iter().take_while(|(_, _, s)| s.gpu && a.supports_effect(s.id)).collect(); let params: Vec = run.iter().map(|(_, g, _)| self.effect_params(ctx, layer, g)).collect(); + let hosts: Vec<_> = run.iter().map(|(i, _, _)| FxHost { index: *i, ..host }).collect(); let steps: Vec = run .iter() .zip(¶ms) - .map(|((i, g, s), params)| FxStep { + .zip(&hosts) + .map(|(((i, g, s), params), host)| FxStep { spec: s, - ctx: EffectCtx { params, time: lt.seconds(), layer_size, seed: g.uid as u32, adjustment, env: EffectEnv { effect_index: *i, ..env } }, + ctx: EffectCtx { + params, + time: lt.seconds(), + layer_size, + seed: g.uid as u32, + adjustment, + env: EffectEnv { host: Some(host), effect_index: *i, ..env }, + }, }) .collect(); let t0 = web_time::Instant::now(); @@ -1212,8 +1220,8 @@ impl<'a> Renderer<'a> { } k += 1; let params = self.effect_params(ctx, layer, g); - host.index.store(i, std::sync::atomic::Ordering::Relaxed); - let env = EffectEnv { effect_index: i, ..env }; + let host = FxHost { index: i, ..host }; + let env = EffectEnv { host: Some(&host), effect_index: i, ..env }; let ectx = EffectCtx { params: ¶ms, time: lt.seconds(), layer_size, seed: g.uid as u32, adjustment, env }; let t0 = web_time::Instant::now(); target.cpu(&mut |buf| {