#323: the Before window waits for the original the After is decoding

At first open of Before/After the two window jobs were asked for together, each
decoded its own 24 MP copy of the original (a review saw two decodes and three
uploads for one photo) and the shared device copy was swapped between them. The
Before's window now waits until the After's job has handed the original to the
session (region::defer_before_window, Session media has_source); later windows
of both share the one copy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-08 18:38:39 -04:00
co-authored by Claude Sonnet 5.5
parent ddaea580a1
commit 057a8d308e
3 changed files with 45 additions and 0 deletions
+23
View File
@@ -403,6 +403,15 @@ impl MediaCache {
(self.thumbs.len(), self.thumbs.cost())
}
/// Whether the decoded source of `id` at `level` is held.
pub fn has_source(&self, id: PhotoId, level: SourceLevel) -> bool {
match level {
SourceLevel::Thumb => self.thumbs.contains(&id),
SourceLevel::Preview => self.previews.iter().any(|e| e.0 == id),
SourceLevel::Full => self.full.as_ref().is_some_and(|e| e.0 == id),
}
}
/// Decoded sources held: (thumbnail level, preview level, full size).
pub fn usage(&self) -> (crate::memory::Usage, crate::memory::Usage, crate::memory::Usage) {
use crate::memory::Usage;
@@ -1389,6 +1398,20 @@ mod tests {
assert!(mean(&a) > mean(&b) + 10.0, "two stops brighter after: {} vs {}", mean(&a), mean(&b));
}
#[test]
fn has_source_follows_what_the_session_accepted() {
let mut s = crate::Session::with_demo();
let id = s.active().unwrap();
let p = s.catalog.photo(id).unwrap().clone();
let long = p.width.max(p.height) as usize;
assert!(!s.media.has_source(id, SourceLevel::Full));
let r = s.render_job(id, long, long, false, true).unwrap().run();
s.accept(&r);
assert!(s.media.has_source(id, SourceLevel::Full));
assert!(!s.media.has_source(id, SourceLevel::Preview));
assert!(!s.media.has_source(PhotoId(id.0 + 1), SourceLevel::Full));
}
#[test]
fn variant_jobs_have_their_own_keys_and_hit_the_cache() {
let mut s = crate::Session::with_demo();
+5
View File
@@ -266,6 +266,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`)
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 job = if v.before {
app.session.region_job_before(c.id, fw, fh, win, !c.crop_tool)
} else {
+17
View File
@@ -170,6 +170,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 the picture on screen has the frame's aspect (to the one pixel a texture's whole-pixel
/// size is off by): a window is a part of the frame, so it can only be placed over a picture that
/// is the frame (an unsupported raw's embedded JPEG may be cropped differently).
@@ -403,6 +410,16 @@ 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
#[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");
}
// Hostile numbers give a plan, never a panic
#[test]
fn hostile_sizes_give_a_usable_plan() {