mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
#323: the windows of a big photo share one device copy of its source
Keeping a 288 MB upload with each view's stages made them thrash against the stage budget; not keeping it made every new window (a pan crossing a snap, the first window of the Before side, any control that invalidates the sampled pixels) upload and convert the source again, about 64 ms against 6-12 ms. A source too big for a view's stages is now kept once for all views of the photo (shared_source), outside the views' stages, and given back when the app idles (trim_pool(0)). source_uploads and shared_source_bytes expose it for tests and diagnostics. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
e9fb4b08e1
commit
f68cd69b1e
3 files changed
+100
-3
No files matched your search
@@ -270,12 +270,39 @@ pub fn set_pool_limit(bytes: u64) {
|
||||
pub fn trim_pool(keep: u64) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(g) = existing_device() {
|
||||
if keep == 0 {
|
||||
render::release_shared_source();
|
||||
}
|
||||
g.trim(keep);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = keep;
|
||||
}
|
||||
|
||||
/// Device bytes of the one big source (a zoom window's original) shared by every view of the photo.
|
||||
pub fn shared_source_bytes() -> usize {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
render::shared_source_bytes()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Big sources uploaded to the shared copy so far (a diagnostic: one big photo is uploaded once, not once per window).
|
||||
pub fn source_uploads() -> u64 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
render::source_uploads()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Device bytes held by a view's GPU stages (kept with its [`StageCache`]); 0 when it has none.
|
||||
pub fn stage_bytes(stages: &StageCache) -> usize {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -417,6 +417,43 @@ 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;
|
||||
|
||||
/// Big sources uploaded to the shared copy so far (diagnostics and tests).
|
||||
static SOURCE_UPLOADS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// The one big source (the original a zoom window is cut from) kept on the device for all the
|
||||
/// views of the photo: the Before and After windows and every pan share it, instead of each
|
||||
/// view's stages holding (or re-uploading) 288 MB of a 24 MP photo. It does not count as a view's
|
||||
/// stages; it is released with the buffer pool when the app idles.
|
||||
static SHARED_SOURCE: std::sync::Mutex<Option<(std::sync::Weak<Rgb32f>, Arc<Buf>)>> = std::sync::Mutex::new(None);
|
||||
|
||||
fn shared_source(src: &Arc<Rgb32f>, upload: impl FnOnce() -> Buf) -> Arc<Buf> {
|
||||
let mut g = SHARED_SOURCE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some((weak, buf)) = &*g
|
||||
&& weak.upgrade().is_some_and(|s| Arc::ptr_eq(&s, src))
|
||||
{
|
||||
return buf.clone();
|
||||
}
|
||||
*g = None; // the previous photo's copy goes before the next is made
|
||||
SOURCE_UPLOADS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let buf = Arc::new(upload());
|
||||
*g = Some((Arc::downgrade(src), buf.clone()));
|
||||
buf
|
||||
}
|
||||
|
||||
/// Free the shared big source (see [`shared_source`]).
|
||||
pub(crate) fn release_shared_source() {
|
||||
*SHARED_SOURCE.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
}
|
||||
|
||||
/// Device bytes of the shared big source, 0 when there is none.
|
||||
pub(crate) fn shared_source_bytes() -> usize {
|
||||
SHARED_SOURCE.lock().unwrap_or_else(|e| e.into_inner()).as_ref().map_or(0, |(_, b)| b.len * 4)
|
||||
}
|
||||
|
||||
pub(crate) fn source_uploads() -> u64 {
|
||||
SOURCE_UPLOADS.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -555,11 +592,12 @@ 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)
|
||||
// (a source too big for a view's stages to keep is the one shared copy: 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) if src.data.len() * 12 <= RETAIN_SOURCE_BYTES => c.source(src, upload),
|
||||
_ => Arc::new(upload()),
|
||||
Some(_) => shared_source(src, upload),
|
||||
None => Arc::new(upload()),
|
||||
};
|
||||
sample(&mut cx, src, src_buf, &plan)
|
||||
}
|
||||
|
||||
@@ -706,8 +706,12 @@ fn windows_match() {
|
||||
/// 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.
|
||||
// (these two share the process's one big source: a test lock keeps them from replacing each other's)
|
||||
static BIG_SOURCE_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn a_window_does_not_keep_the_whole_source_on_the_device() {
|
||||
let _lock = BIG_SOURCE_TESTS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !gpu() {
|
||||
return;
|
||||
}
|
||||
@@ -727,3 +731,31 @@ fn a_window_does_not_keep_the_whole_source_on_the_device() {
|
||||
assert_eq!((again.image.width, again.image.height), (640, 480));
|
||||
assert!(lightcraft_gpu::stage_bytes(&stages) < 100 << 20);
|
||||
}
|
||||
|
||||
/// …but the 288 MB is uploaded once for every window of every view of the photo, not once per
|
||||
/// window: panning at 1:1, and the Before and After windows, share one device copy of it, which
|
||||
/// goes when the app idles (`trim_pool(0)`).
|
||||
#[test]
|
||||
fn windows_of_a_big_photo_share_one_uploaded_source() {
|
||||
let _lock = BIG_SOURCE_TESTS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !gpu() {
|
||||
return;
|
||||
}
|
||||
use lightcraft_pipeline::PixelWindow;
|
||||
let src = scene(1, 6000, 4000);
|
||||
let info = SourceInfo { raw: true, ..Default::default() };
|
||||
let s = DevelopSettings::default();
|
||||
let (after, before) = (StageCache::default(), StageCache::default());
|
||||
let at = |x: usize| RenderRequest { window: Some(PixelWindow { x, y: 1500, w: 640, h: 480 }), ..RenderRequest::fit(6000, 4000) };
|
||||
let start = lightcraft_gpu::source_uploads();
|
||||
for (cache, x) in [(&after, 1000), (&after, 1256), (&after, 1512), (&before, 1512), (&before, 1768)] {
|
||||
lightcraft_gpu::render(&src, &info, &s, &at(x), Some(cache)).expect("gpu render");
|
||||
}
|
||||
assert_eq!(lightcraft_gpu::source_uploads() - start, 1, "one upload for five windows of two views");
|
||||
assert!(lightcraft_gpu::shared_source_bytes() >= 280_000_000, "{}", lightcraft_gpu::shared_source_bytes());
|
||||
assert!(lightcraft_gpu::stage_bytes(&after) < 100 << 20, "and it is not in the views' stages");
|
||||
lightcraft_gpu::trim_pool(0);
|
||||
assert_eq!(lightcraft_gpu::shared_source_bytes(), 0, "an idle app gives it back");
|
||||
lightcraft_gpu::render(&src, &info, &s, &at(1000), Some(&after)).expect("gpu render");
|
||||
assert_eq!(lightcraft_gpu::source_uploads() - start, 2);
|
||||
}
|
||||
Reference in new issue
Block a user