mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
M5.6: a zoomed view keeps the GPU copy of its original while idle (re-uploaded after 3 s)
The idle trim (3 s without renders) released the shared device copy of the original a zoom window is cut from, so pausing between pans made the next pan upload it again: 800 MB for a 64 MP raw, ~1.4 s of "gpu sample" measured (5 ms when it is still there). While a Detail window texture is held the renderer is not idle; once the windows are gone (zoomed out, another photo, or another view, which now frees the Detail view's windows) the trim runs as before.
This commit is contained in:
@@ -377,10 +377,18 @@ fn draw_window(p: &egui::Painter, app: &LightcraftApp, pane: u8, id: PhotoId, lo
|
||||
|
||||
/// Per frame, before the central view is shown: Compare and Reference keep two photos' originals
|
||||
/// decoded (their windows are cut from both); other views keep one and free the panes' windows.
|
||||
/// The Detail view's photo keeps its original even over the memory budget (a zoomed 64 MP raw).
|
||||
/// The Detail view's photo keeps its original even over the memory budget (a zoomed 64 MP raw);
|
||||
/// other views free the Detail view's windows (while one is shown the GPU copy of its original stays).
|
||||
pub fn prepare(app: &mut LightcraftApp) {
|
||||
let shown = if app.ui.view == ViewMode::Detail { app.session.active() } else { None };
|
||||
app.session.media.set_on_screen(shown);
|
||||
if app.ui.view != ViewMode::Detail {
|
||||
app.region_view = None;
|
||||
app.region_before_view = None;
|
||||
app.region_tiles.clear();
|
||||
app.renderer.release(Slot::Region);
|
||||
app.renderer.release(Slot::RegionBefore);
|
||||
}
|
||||
if matches!(app.ui.view, ViewMode::Compare | ViewMode::Reference) {
|
||||
app.session.media.set_full_capacity(WINDOW_PANES as usize);
|
||||
return;
|
||||
|
||||
@@ -767,10 +767,12 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// Free the GPU renderer's recycled buffers once nothing has rendered for [`IDLE_TRIM`] (they
|
||||
/// are reallocated by the next render; while working, renders reuse them).
|
||||
/// are reallocated by the next render; while working, renders reuse them). A zoomed view
|
||||
/// showing its windows is not idle: the device copy of the original they are cut from stays for
|
||||
/// the next pan (a 64 MP raw is 800 MB to upload again, over a second).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn trim_when_idle(&mut self, ctx: &egui::Context) {
|
||||
if !self.pending.is_empty() {
|
||||
if !self.pending.is_empty() || self.textures.contains_key(&Slot::Region) || self.textures.contains_key(&Slot::RegionBefore) {
|
||||
self.idle = None;
|
||||
return;
|
||||
}
|
||||
@@ -1291,6 +1293,25 @@ mod stage_budget_tests {
|
||||
|
||||
const MB: usize = 1 << 20;
|
||||
|
||||
// Given a zoomed view showing a window, the app idling does not free the GPU copy of the
|
||||
// original the next window is cut from; once the windows are gone the idle trim runs again
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn a_zoomed_view_is_not_trimmed_while_idle() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut r = Renderer::default();
|
||||
let tex = ctx.load_texture("window", egui::ColorImage::new([1, 1], vec![egui::Color32::BLACK]), Default::default());
|
||||
r.textures.insert(Slot::Region, Tex { key: 1, photo: PhotoId(1), tex, size: [1, 1], histogram: None, ms: 0.0, quick: None, pixels: None });
|
||||
let long_ago = std::time::Instant::now().checked_sub(IDLE_TRIM * 2).unwrap();
|
||||
r.idle = Some((long_ago, false));
|
||||
r.trim_when_idle(&ctx);
|
||||
assert_eq!(r.idle, None, "a zoomed view is not idle");
|
||||
r.release(Slot::Region);
|
||||
r.idle = Some((long_ago, false));
|
||||
r.trim_when_idle(&ctx);
|
||||
assert_eq!(r.idle, Some((long_ago, true)), "trimmed once the window is gone");
|
||||
}
|
||||
|
||||
// Given stage caches within the budget, nothing is cleared
|
||||
#[test]
|
||||
fn nothing_is_trimmed_within_the_budget() {
|
||||
|
||||
@@ -335,6 +335,21 @@ mod in_the_loupe {
|
||||
assert_eq!(h.app.region_view, None);
|
||||
}
|
||||
|
||||
// Given a zoomed view with a window render, when I go to the grid, then its windows are freed
|
||||
// too (while one is held the app never counts as idle and keeps the GPU copy of the original)
|
||||
#[test]
|
||||
fn leaving_a_zoomed_detail_view_frees_its_windows() {
|
||||
let (mut h, _) = detail();
|
||||
h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;
|
||||
h.request("engine.execute", json!({"command": "view.zoom100"}), T);
|
||||
h.settle(SETTLE);
|
||||
assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some());
|
||||
h.request("ui.set", json!({"view": "photoGrid"}), T);
|
||||
h.step();
|
||||
assert_eq!((tile(&h, Slot::Region), tile(&h, Slot::RegionBefore)), (None, None));
|
||||
assert_eq!((h.app.region_view, h.app.region_before_view), (None, None));
|
||||
}
|
||||
|
||||
// Given a zoomed view, when the photo is edited, then the old window (made with the old look)
|
||||
// is no longer current and is replaced by one for the new look
|
||||
#[test]
|
||||
|
||||
@@ -267,7 +267,7 @@ the device on their first GPU render. `lightcraft_gpu::ready()` asks without blo
|
||||
developed image in ~50 ms.
|
||||
|
||||
## Memory (M5.6)
|
||||
A view's stages keep an uploaded source only up to 96 MB (a Preview-level source); a bigger one (the original a zoom window is cut from) is kept once, outside the views' stages, for all views of the photo (the Before and After windows and every pan share it; the Before window of a Before/After waits at first open until the After's job has handed the original over, or each would decode and upload its own copy) and given back when the app idles (issue #323: kept per view it counted against the stage budget on every tick of a drag, and not kept every new window uploaded 288 MB again).
|
||||
A view's stages keep an uploaded source only up to 96 MB (a Preview-level source); a bigger one (the original a zoom window is cut from) is kept once, outside the views' stages, for all views of the photo (the Before and After windows and every pan share it; the Before window of a Before/After waits at first open until the After's job has handed the original over, or each would decode and upload its own copy) and given back when the app idles with no zoom window on screen (a zoomed view keeps it for its next pan: a 64 MP original is 800 MB to upload again; issue #323: kept per view it counted against the stage budget on every tick of a drag, and not kept every new window uploaded 288 MB again).
|
||||
- `library.memory` reports what the engine's caches hold (decoded thumbnail / preview / full-size
|
||||
sources, rendered previews) and the GPU renderer's device buffers (allocated, of which pooled
|
||||
and retired); `ui.inspect` → `memory` adds the loupe's stage caches (CPU images, GPU buffers)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- 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.
|
||||
- A zoomed view keeps the GPU copy of the original while it is on screen: pausing for a few seconds between pans no
|
||||
longer frees it, which cost a re-upload of about a second on the next pan (800 MB for a 64 MP raw). It is freed
|
||||
once you zoom out or move on.
|
||||
|
||||
## 2026-10-11
|
||||
|
||||
|
||||
Reference in New Issue
Block a user