From ea1f2fe9840c5741aaa4d4f6dab37eca8c579a64 Mon Sep 17 00:00:00 2001 From: ded Date: Tue, 20 Oct 2026 04:21:14 +0300 Subject: [PATCH] G5: a still decoded larger than its nominal size is resampled like the renderer does (#1812) Review: still_at_size returned a smart object's pixels unresampled when they differed from the footage's nominal size. It now follows footage_buf: pixels larger than nominal are resampled to the requested size, smaller ones are left for the renderer to scale up. Co-Authored-By: Claude Opus 5.5 --- crates/media/src/pool.rs | 13 ++++++++----- crates/media/tests/footage.rs | 8 ++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/media/src/pool.rs b/crates/media/src/pool.rs index 6bc4bd8d..062bdac5 100644 --- a/crates/media/src/pool.rs +++ b/crates/media/src/pool.rs @@ -774,13 +774,16 @@ impl Inner { #[cfg(target_arch = "wasm32")] fn prefetch(self: &Arc, _footage: &Footage, _t: Tick, _size: Option<(u32, u32)>, _float: FloatCurve) {} - /// A still (image file or sequence frame) of the footage's own size, at `loc.key.size` when - /// one is set: resampled as the renderer would ([`FootageSource::frame_at_size`]), so the - /// pool keeps the reduced frame and a reduced-resolution render neither decodes nor resamples - /// a large still again for every frame (#1812: 21120-px plates at Quarter). + /// A still (image file or sequence frame) at `loc.key.size` when one is set: resampled as the + /// renderer would ([`FootageSource::frame_at_size`]), so the pool keeps the reduced frame and a + /// reduced-resolution render neither decodes nor resamples a large still again for every frame + /// (#1812: 21120-px plates at Quarter). Like the renderer's `footage_buf`, an image decoded + /// larger than the footage's nominal size (a smart object's pixels) is resampled too, and one + /// decoded smaller is not (the renderer scales the layer instead). fn still_at_size(img: Image, loc: &Loc, footage: &Footage) -> Image { let (w, h) = loc.key.size; - if w == 0 || h == 0 || (img.width, img.height) != (footage.width, footage.height) || (w, h) == (img.width, img.height) { + let smaller = img.width == 0 || footage.width as f64 / img.width as f64 > 1.0 + 1e-9; + if w == 0 || h == 0 || smaller || (w, h) == (img.width, img.height) { return img; } effectcraft_raster::resample(&img, w, h) diff --git a/crates/media/tests/footage.rs b/crates/media/tests/footage.rs index 2e882d73..0722e5d4 100644 --- a/crates/media/tests/footage.rs +++ b/crates/media/tests/footage.rs @@ -608,6 +608,14 @@ fn stills_at_reduced_sizes_are_resampled_once() { assert!(Arc::ptr_eq(&sized, &again), "{w}×{h}: kept at that size"); assert_eq!(pool.stats().misses, misses, "{w}×{h}: not decoded again"); } + // Pixels larger than the nominal size (a smart object's) are resampled as the renderer does; + // smaller ones are left as they are (the renderer scales the layer up instead). + let larger = Footage { width: 306, height: 48, ..f.clone() }; + let sized = MediaPool::new().frame_at_size(ItemId(1), &larger, Tick::ZERO, 153, 24).expect("frame"); + assert_eq!(*sized, effectcraft_raster::resample(&full, 153, 24)); + let smaller = Footage { width: 1226, height: 194, ..f.clone() }; + let kept = MediaPool::new().frame_at_size(ItemId(1), &smaller, Tick::ZERO, 306, 48).expect("frame"); + assert_eq!(*kept, *full); let _ = std::fs::remove_dir_all(&dir); }