From 4cf6580b19539e02863bdf03b071a55698ee144c Mon Sep 17 00:00:00 2001 From: ded Date: Tue, 20 Oct 2026 10:42:22 +0300 Subject: [PATCH] G5: a reduced still's cache key includes its nominal size (#1812) Review: two footage items of one file with different nominal sizes could share a sized entry, though one is resampled and the other kept as decoded. Co-Authored-By: Claude Opus 5.5 --- crates/media/src/pool.rs | 10 ++++++++-- crates/media/tests/footage.rs | 7 ++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/media/src/pool.rs b/crates/media/src/pool.rs index 062bdac5..09c9bb1d 100644 --- a/crates/media/src/pool.rs +++ b/crates/media/src/pool.rs @@ -45,6 +45,9 @@ struct Key { matte: [u32; 3], /// A movie frame made at this size for a reduced-resolution render ((0, 0): as decoded). size: (u32, u32), + /// A still at a reduced `size`: the footage's nominal size, which decides whether it is + /// resampled (see `Inner::still_at_size`); (0, 0) otherwise. + nominal: (u32, u32), } struct Entry { @@ -678,7 +681,7 @@ impl Inner { FloatCurve::Gamma24 => 2, } << 2); let matte = if footage.alpha == AlphaMode::Premultiplied { footage.premul_color.map(f32::to_bits) } else { [0; 3] }; - let key = |path: &str, frame| Key { path: path.into(), frame, interp, matte, size: (0, 0) }; + let key = |path: &str, frame| Key { path: path.into(), frame, interp, matte, size: (0, 0), nominal: (0, 0) }; match footage.kind { FootageKind::Sequence if !footage.sequence.is_empty() => { let i = Self::frame_index(footage.frame_rate, t, footage.sequence_frames(), footage.loop_count); @@ -719,6 +722,9 @@ impl Inner { // A still asked for at its own size is the unsized frame (one decode, one entry). if let Some(size) = size.filter(|s| movie || *s != (footage.width, footage.height)) { loc.key.size = size; + if !movie { + loc.key.nominal = (footage.width, footage.height); + } } let mut c = lock(&self.cache); // sequential playback of a movie: read the next frame ahead @@ -1074,7 +1080,7 @@ mod tests { fn lru_respects_budget_and_recycles() { let mut c = Lru::default(); let img = || Arc::new(Image::new(16, 16)); // 4 KiB + overhead - let k = |i| Key { path: "a".into(), frame: i, interp: 0, matte: [0; 3], size: (0, 0) }; + let k = |i| Key { path: "a".into(), frame: i, interp: 0, matte: [0; 3], size: (0, 0), nominal: (0, 0) }; let budget = 3 * (4096 + 64); for i in 0..10 { c.insert(k(i), img(), budget); diff --git a/crates/media/tests/footage.rs b/crates/media/tests/footage.rs index 0722e5d4..f510109e 100644 --- a/crates/media/tests/footage.rs +++ b/crates/media/tests/footage.rs @@ -610,11 +610,12 @@ fn stills_at_reduced_sizes_are_resampled_once() { } // 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). + // One pool, one file, one requested size: the two decisions don't share a cache entry. 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"); + let sized = pool.frame_at_size(ItemId(2), &larger, Tick::ZERO, 153, 24).expect("frame"); + assert_eq!(*sized, effectcraft_raster::resample(&full, 153, 24)); + let kept = pool.frame_at_size(ItemId(3), &smaller, Tick::ZERO, 153, 24).expect("frame"); assert_eq!(*kept, *full); let _ = std::fs::remove_dir_all(&dir); }