LR-MERGE-HDR: decode full-size merge frames one at a time; AHD writes its tiles straight into the picture

A 7-frame bracket of 67 MP raws ran out of memory: the merge decoded the frames four at a time, and the AHD demosaic
collected every tile before copying them into a second full-size picture. Full-size frames now decode one after the
other (the demosaic is parallel inside; previews stay parallel), and AHD fills the picture by rows of tiles. Peak memory
of 67 MP HDR merges: 3 frames 4.8 → 3.8 GB, 4 frames 6.3 → 4.7 GB, about the same time; AHD output is pixel-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
dev-null-undefined
2026-10-14 12:56:59 +02:00
co-authored by Claude Opus 5.5
parent a142179f20
commit 6fb0d22326
3 changed files with 28 additions and 26 deletions
+12 -14
View File
@@ -105,20 +105,18 @@ impl MergeJob {
use rayon::prelude::*;
let done = std::sync::atomic::AtomicUsize::new(0);
let n = self.sources.len();
let frames: std::result::Result<Vec<Frame>, String> = self
.sources
.par_iter()
.map(|(_, path)| {
let bytes = (self.read)(path)?;
// a raw without colour matrices of its own starts from the colour it has when opened alone (#965)
let f = lightcraft_merge::load_frame_with(&bytes, self.preview, orient, &crate::camera_preview::starting_colour_tags)
.map_err(|e| format!("{path}: {e}"));
let k = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
progress(span.0 + (span.1 - span.0) * k as f32 / n as f32, "Reading photos");
f
})
.collect();
frames
let one = |(_, path): &(PhotoId, String)| {
let bytes = (self.read)(path)?;
// a raw without colour matrices of its own starts from the colour it has when opened alone (#965)
let f = lightcraft_merge::load_frame_with(&bytes, self.preview, orient, &crate::camera_preview::starting_colour_tags)
.map_err(|e| format!("{path}: {e}"));
let k = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
progress(span.0 + (span.1 - span.0) * k as f32 / n as f32, "Reading photos");
f
};
// Full-size photos are decoded one at a time (the demosaic is parallel inside): decoding several 60 MP raws at
// once multiplied the decoders' scratch memory, and a 7-frame bracket ran out of memory.
if self.preview.is_some() { self.sources.par_iter().map(one).collect() } else { self.sources.iter().map(one).collect() }
}
/// Run the merge. `progress(fraction, stage)` returns false to cancel.
+13 -12
View File
@@ -19,20 +19,21 @@ const MARGIN: usize = 6;
pub(crate) fn ahd(m: &Mosaic) -> Rgb32f {
let (w, h) = (m.w, m.h);
let tiles: Vec<(usize, usize)> = (0..h.div_ceil(TILE)).flat_map(|ty| (0..w.div_ceil(TILE)).map(move |tx| (tx * TILE, ty * TILE))).collect();
let results: Vec<(usize, usize, usize, usize, Vec<[f32; 3]>)> = tiles
.par_iter()
.map(|&(x0, y0)| {
let (tw, th) = (TILE.min(w - x0), TILE.min(h - y0));
(x0, y0, tw, th, tile(m, x0, y0, tw, th))
})
.collect();
let mut out = Rgb32f::new(w, h);
for (x0, y0, tw, th, px) in results {
for y in 0..th {
out.data[(y0 + y) * w + x0..(y0 + y) * w + x0 + tw].copy_from_slice(&px[y * tw..(y + 1) * tw]);
}
if w == 0 || h == 0 {
return out;
}
// one row of tiles per task, written straight into the picture (collecting the tiles first held a second copy)
out.data.par_chunks_mut(w * TILE).enumerate().for_each(|(ty, band)| {
let (y0, th) = (ty * TILE, band.len() / w);
for x0 in (0..w).step_by(TILE) {
let tw = TILE.min(w - x0);
let px = tile(m, x0, y0, tw, th);
for y in 0..th {
band[y * w + x0..y * w + x0 + tw].copy_from_slice(&px[y * tw..(y + 1) * tw]);
}
}
});
out
}
+3
View File
@@ -74,4 +74,7 @@ scale in `BaselineExposure`; raw sources keep their DNG colour tags (camera RGB)
- *Create Stack*: the result goes on top of a collapsed stack of its sources (`Catalog::stack_with_ops`); its
caption also lists them.
- Panorama output is limited to `maxMegapixels` (default 40) — blending memory is ~25 bytes/pixel.
- HDR memory: every frame is held as 32-bit float RGB (~0.86 GB per 67 MP frame at the peak, plus ~1.2 GB); full-size
photos are decoded one at a time. Measured on 67 MP raws: 3 frames 3.8 GB, 4 frames 4.7 GB, 5 frames 5.5 GB (was
4.8 / 6.3 GB for 3 / 4). Half-float frames would halve the per-frame share.
- The browser build has no file access for merges yet.