diff --git a/crates/gpu/src/render.rs b/crates/gpu/src/render.rs index c38418616..46b47f1a4 100644 --- a/crates/gpu/src/render.rs +++ b/crates/gpu/src/render.rs @@ -414,6 +414,9 @@ fn warp_params( /// ~20 % faster than 8 at 6000 × 4000 (fewer interval evaluations, few more edge pixels). const COVER_ROWS: usize = 16; +/// The largest source (bytes on the device) a view's stages keep uploaded between renders. +const RETAIN_SOURCE_BYTES: usize = 96 << 20; + /// The reference framing decision ([`Warp::covers`](lightcraft_pipeline::optics::Warp::covers)) for every /// output pixel, one bit per pixel, rows padded to 32-bit words. Blocks of 32 × [`COVER_ROWS`] pixels whose /// interval bounds place them clearly inside or outside the image are filled at once @@ -552,9 +555,11 @@ pub fn render( Some(e) => e.sampled.clone(), None if gpu.fits(src.data.len() * 3) => { let upload = || gpu.upload(rgb_words(src)); + // (a source too big to keep is uploaded for this render only: a window is cut from the + // photo's own pixels, and what its stages keep is its own sampled pixels) let src_buf = match stages { - Some(c) => c.source(src, upload), - None => Arc::new(upload()), + Some(c) if src.data.len() * 12 <= RETAIN_SOURCE_BYTES => c.source(src, upload), + _ => Arc::new(upload()), }; sample(&mut cx, src, src_buf, &plan) } diff --git a/crates/gpu/tests/equivalence.rs b/crates/gpu/tests/equivalence.rs index b4b52e215..bfb72e02f 100644 --- a/crates/gpu/tests/equivalence.rs +++ b/crates/gpu/tests/equivalence.rs @@ -701,3 +701,29 @@ fn windows_match() { let corner = RenderRequest { window: Some(PixelWindow { x: 2360, y: 1520, w: 640, h: 480 }), ..RenderRequest::fit(3000, 2000) }; check("window: vignette corner", &src, &info, &s, &corner); } + +/// Issue #323: a window of a 24 MP photo is cut from a source of 288 MB on the device. Keeping that +/// upload with the view's stages counted it against the stage budget on every tick of a drag and +/// got the stages cleared, and the upload and resampling redone: the window's stages are its own +/// pixels, not the photo's. +#[test] +fn a_window_does_not_keep_the_whole_source_on_the_device() { + if !gpu() { + return; + } + use lightcraft_pipeline::PixelWindow; + let src = scene(1, 6000, 4000); + let info = SourceInfo { raw: true, ..Default::default() }; + let stages = StageCache::default(); + let s = DevelopSettings::default(); + let req = RenderRequest { window: Some(PixelWindow { x: 2000, y: 1500, w: 640, h: 480 }), ..RenderRequest::fit(6000, 4000) }; + let _ = lightcraft_gpu::render(&src, &info, &s, &req, Some(&stages)).expect("gpu render"); + let held = lightcraft_gpu::stage_bytes(&stages); + assert!(held < 100 << 20, "the view's GPU stages hold {} MiB for a 640×480 window", held >> 20); + // …and the next tick of a drag reuses what is there + let mut s2 = s.clone(); + s2.light.exposure = 0.5; + let again = lightcraft_gpu::render(&src, &info, &s2, &req, Some(&stages)).expect("gpu render"); + assert_eq!((again.image.width, again.image.height), (640, 480)); + assert!(lightcraft_gpu::stage_bytes(&stages) < 100 << 20); +}