From 87eb51a0899026d155de6b8c76290b4ded4352fc Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 27 Oct 2026 09:22:14 -0700 Subject: [PATCH] Include alpha interpretation in frame-blend cache keys --- crates/engine/src/tests/proxy.rs | 43 ++++++++++++++++++++++++++++++++ crates/render/src/pipeline.rs | 4 +++ 2 files changed, 47 insertions(+) diff --git a/crates/engine/src/tests/proxy.rs b/crates/engine/src/tests/proxy.rs index 0e57e5fa1..941a9a96b 100644 --- a/crates/engine/src/tests/proxy.rs +++ b/crates/engine/src/tests/proxy.rs @@ -246,6 +246,49 @@ fn comp_proxy_frames_blend() { assert!(blue(px(&s, outer, 0.1, o, 1, 1))); } +#[test] +fn comp_proxy_frame_blend_cache_tracks_alpha_interpretation() { + struct Interpreted(std::sync::atomic::AtomicUsize); + impl FootageSource for Interpreted { + fn frame(&self, _: ItemId, f: &Footage, _: Tick) -> Option> { + self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let pixel = match f.alpha { + AlphaMode::Straight => [0.4, 0.2, 0.1, 0.5], + AlphaMode::Premultiplied => [f.premul_color[0] * 0.5, 0.1, 0.0, 0.5], + AlphaMode::Ignore => [0.8, 0.4, 0.2, 1.0], + }; + Some(Arc::new(Image::filled(4, 4, pixel))) + } + } + for mode in ["frameMix", "pixelMotion"] { + let mut s = session(); + let footage = Arc::new(Interpreted(std::sync::atomic::AtomicUsize::new(0))); + s.footage = footage.clone(); + let (inner, outer) = nested(&mut s); + s.execute("file.setProxy", json!({"item": inner.0, "path": "/m/alpha.mov"})).unwrap(); + s.execute("file.interpretProxy", json!({"item": inner.0, "alpha": "straight"})).unwrap(); + s.execute("layer.frameBlending", json!({"mode": mode})).unwrap(); + s.execute("comp.setSwitch", json!({"switch": "frameBlending", "value": true})).unwrap(); + let opts = RenderOpts::default(); + let first = px(&s, outer, 0.05, opts, 1, 1); + let calls = footage.0.load(std::sync::atomic::Ordering::Relaxed); + assert_eq!(px(&s, outer, 0.05, opts, 1, 1), first); + assert_eq!(footage.0.load(std::sync::atomic::Ordering::Relaxed), calls); + for params in [ + json!({"item": inner.0, "alpha": "ignore"}), + json!({"item": inner.0, "alpha": "premultiplied", "premulColor": [0.0, 0.0, 0.0]}), + json!({"item": inner.0, "alpha": "premultiplied", "premulColor": [1.0, 0.0, 0.0]}), + ] { + s.execute("file.interpretProxy", params.clone()).unwrap(); + let mut fresh = s.renderer(opts); + fresh.cache = None; + let want = fresh.comp_frame(outer, Tick::from_seconds_f64(0.05)).get(1, 1); + let actual = px(&s, outer, 0.05, opts, 1, 1); + assert_eq!(actual, want, "{mode} after {params}"); + } + } +} + #[test] fn interpret_proxy_settings_apply_to_the_proxy() { let mut s = session(); diff --git a/crates/render/src/pipeline.rs b/crates/render/src/pipeline.rs index faa21294e..84e4fec0d 100644 --- a/crates/render/src/pipeline.rs +++ b/crates/render/src/pipeline.rs @@ -740,6 +740,10 @@ impl<'a> Renderer<'a> { use std::hash::{Hash, Hasher}; let mut h = std::collections::hash_map::DefaultHasher::new(); (0x00f8_a3e5_u64, item, i, (w as f32).to_bits(), mode as u8, f.frame_rate.num, f.frame_rate.den, &f.path, float).hash(&mut h); + (f.alpha as u8).hash(&mut h); + if f.alpha == effectcraft_project::AlphaMode::Premultiplied { + f.premul_color.map(f32::to_bits).hash(&mut h); + } h.finish() }); if let (Some(c), Some(k)) = (self.cache, key)