diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 234476167..83b7b8b48 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -309,10 +309,11 @@ fn request_window(app: &mut LightcraftApp, c: &WindowCtx, v: &WindowView) -> Opt to_px * (v.visible.bottom() - v.target.top()), ); let win = crate::region::window_for(fw, fh, visible, crate::region::max_span(c.texture_side))?; - // the Before side waits for the original the After is decoding (see `defer_before_window`) + // a window waits for the original another window job is decoding (see `defer_window`) let original_held = app.session.media.has_source(c.id, lightcraft_engine::SourceLevel::for_size(fw.max(fh))); - if crate::region::defer_before_window(v.before, app.renderer.is_pending(Slot::Region), original_held) { - return app.region_before_view.filter(|w| w.photo == c.id); + let window_pending = app.renderer.is_pending(Slot::Region) || app.renderer.is_pending(Slot::RegionBefore); + if crate::region::defer_window(window_pending, original_held) { + return if v.before { app.region_before_view } else { app.region_view }.filter(|w| w.photo == c.id); } let job = if v.before { app.session.region_job_before(c.id, fw, fh, win, !c.crop_tool) diff --git a/crates/ui-egui/src/region.rs b/crates/ui-egui/src/region.rs index 8fe29e135..184a8319a 100644 --- a/crates/ui-egui/src/region.rs +++ b/crates/ui-egui/src/region.rs @@ -174,11 +174,13 @@ pub fn window_clip(mode: crate::state::BeforeAfter, before_side: bool, pane: egu clip.intersect(canvas) } -/// Whether the Before window waits for the After's. Both are cut from the same decoded original; -/// asked for together while it isn't held yet, each worker decodes its own copy (two full-size -/// decodes, two uploads, one thrown away). The Before follows once the original is in. -pub fn defer_before_window(before_side: bool, after_pending: bool, original_held: bool) -> bool { - before_side && after_pending && !original_held +/// Whether a window waits for the one already being rendered. All windows are cut from the same +/// decoded original; asked for while it isn't held yet (the Before and After together, in either +/// order, or a new window while the first is still decoding), each worker decodes its own copy +/// (two or three full-size decodes and uploads, all but one thrown away). The waiting window +/// follows once the original is in. +pub fn defer_window(window_pending: bool, original_held: bool) -> bool { + window_pending && !original_held } /// Whether the picture on screen has the frame's aspect (to the one pixel a texture's whole-pixel @@ -427,14 +429,13 @@ mod tests { assert_eq!(window_clip(Original, true, canvas, canvas, img), canvas); } - // Given Before and After windows asked for at once at first open, only the After's goes: the - // Before's waits for the decoded original instead of decoding another copy + // Given a window asked for while another is rendering and the original isn't decoded yet, it + // waits for the decoded original instead of decoding another copy #[test] - fn the_before_window_waits_for_the_original_the_after_is_decoding() { - assert!(defer_before_window(true, true, false)); - assert!(!defer_before_window(true, true, true), "the original is in"); - assert!(!defer_before_window(true, false, false), "nothing is decoding it"); - assert!(!defer_before_window(false, true, false), "the After never waits"); + fn a_window_waits_for_the_original_another_is_decoding() { + assert!(defer_window(true, false)); + assert!(!defer_window(true, true), "the original is in"); + assert!(!defer_window(false, false), "nothing is decoding it"); } // Hostile numbers give a plan, never a panic diff --git a/crates/ui-egui/src/tests_preview_limit.rs b/crates/ui-egui/src/tests_preview_limit.rs index 8efdebe3a..a8b547387 100644 --- a/crates/ui-egui/src/tests_preview_limit.rs +++ b/crates/ui-egui/src/tests_preview_limit.rs @@ -452,6 +452,28 @@ mod in_the_loupe { assert_eq!(main.size, before_main.size, "and the same whole-frame size under them"); } + // Given Before/After side by side, when I zoom to 1:1, then one window job decodes the original + // and the other side waits for it (the Before, asked for first, used to start a second decode + // beside the After's); both windows still arrive + #[test] + fn side_by_side_at_one_to_one_decodes_the_original_once() { + let (mut h, _) = detail(); + let id = h.app.session.active().expect("a photo is open"); + h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; + h.app.ui.zoom = crate::state::Zoom::Percent(100.0); + let pending = |h: &Headless| [Slot::Region, Slot::RegionBefore].into_iter().filter(|s| h.app.renderer.is_pending(*s)).count(); + for _ in 0..100 { + h.step(); + if pending(&h) > 0 { + break; + } + } + assert!(!h.app.session.media.has_source(id, lightcraft_engine::SourceLevel::Full), "the original is still decoding"); + assert_eq!(pending(&h), 1, "one window job decodes it"); + h.settle(SETTLE); + assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some(), "both windows arrived"); + } + // Given the Before view alone (\), only the Before gets a window #[test] fn the_original_view_windows_only_the_before() { diff --git a/docs/whats-new.md b/docs/whats-new.md index 994a5a055..0d4300dbc 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -7,6 +7,9 @@ high-resolution cameras such as the Sony A7R VI (64 MP). Their decoded original (~800 MB) is bigger than the default cache share (768 MB), so it was evicted as soon as it was stored. The original of the photo shown in Detail now stays, like its preview, until another photo or view is shown. +- Zooming in with Before/After side by side decodes the original once: the Before side used to decode its own copy + beside the After's, and a window asked for again while the first was still decoding (a pan, the layout settling) + started yet another decode. ## 2026-10-11