Open and export a linear DNG too large for one texture

A 22927×8966 Lightroom panorama opened as its embedded preview with
develop withheld, because no texture could hold it. DevelopSession now
opens a linear DNG past PROXY_EDGE (8192) on a box-reduced copy, and
keeps the full resolution on the CPU. The canvas at fit, the thumbnail,
the histograms and the masks work from the copy; a render finer than
it — the canvas zoomed in, a tile of the export — samples a window cut
from the full resolution, kept while the view stays inside it. The
export renders in halo-grown tiles of 4096 and assembles them.

On the panorama: decode 1.6 s, open 210 ms, canvas 37 ms, a zoomed
window 130 ms, the full-size export 5.9 s.
This commit is contained in:
2026-09-27 17:37:06 -04:00
parent 0007fa459f
commit 885864b6a0
4 changed files with 426 additions and 40 deletions
+54
View File
@@ -112,6 +112,19 @@ pub struct DevelopSession {
/// history still stands on that crop. See [`super::framing::CropNotice`].
pub(super) crop_notice: Option<super::framing::CropNotice>,
pub(super) demosaiced: Arc<DemosaicedImage>,
/// TRACES: FR-DSP-2 | NFR-RES-2
/// The photograph at full resolution, when it is too large to hold in one
/// texture. `demosaiced` is then a reduced copy of it, which is all the
/// canvas needs at fit and all the histograms, masks and probes ever
/// read; a render that wants more detail than the copy has — the canvas
/// zoomed in, a tile of the export — cuts a window from this instead
/// (see `source_for`). `None` for every photograph that fits, which is
/// nearly all of them.
pub(super) full: Option<Arc<RawImage>>,
/// The last window cut from `full`: the region it covers in normalised
/// source coordinates, the reduction it was cut at, and the texture. Kept
/// so panning a zoomed canvas does not re-upload what is already there.
pub(super) window: Option<(CropRect, u32, Arc<DemosaicedImage>)>,
pub(super) adjust: AdjustPass,
/// TRACES: FR-DSP-7
/// Optional, because a session that cannot count its frames is still a
@@ -312,6 +325,16 @@ pub struct DevelopSession {
pub(super) display_space: dr_types::ColourSpace,
}
/// TRACES: FR-DSP-2 | NFR-RES-2
/// The longest edge a photograph is developed at whole; one larger is
/// developed from a reduced copy and full-resolution windows.
///
/// Below the device's own limit on purpose. A 16384-texel texture of
/// half floats is two gigabytes, and the canvas never shows more than a few
/// thousand pixels of it; 8192 is four times a 4K long edge, the same bound
/// the JPEG path fits a film scan to.
pub const PROXY_EDGE: u32 = 8192;
impl DevelopSession {
/// Demosaic an image and prepare its edit graph.
///
@@ -329,6 +352,35 @@ impl DevelopSession {
Ok(Self::with_source(ctx, demosaiced, orientation))
}
/// TRACES: FR-DSP-2 | NFR-RES-2
/// Open a decoded photograph, whatever its size.
///
/// A photograph that fits the device goes through [`Self::open`] as it
/// always did. A linear DNG larger than [`PROXY_EDGE`] — a stitched
/// panorama — is instead held at full resolution on the CPU and opened
/// on a copy reduced to fit, from which the canvas, the histograms and
/// the masks work; the full resolution is cut into windows only where a
/// render needs it. Without this the file opened as its embedded preview
/// with develop withheld.
pub fn open_owned(
ctx: &GpuContext,
raw: RawImage,
orientation: dr_types::Orientation,
) -> Result<Self, String> {
let (w, h) = (raw.crop.width.max(1), raw.crop.height.max(1));
let edge = PROXY_EDGE.min(DemosaicedImage::max_dimension(ctx));
if raw.samples_per_pixel != 3 || w.max(h) <= edge {
return Self::open(ctx, &raw, orientation);
}
let reduce = w.max(h).div_ceil(edge);
log::info!("{w}×{h} is larger than one texture; developing from a 1/{reduce} copy");
let proxy = DemosaicedImage::linear_rgb16_window(ctx, &raw, [0, 0, w, h], reduce)
.map_err(|e| e.to_string())?;
let mut session = Self::with_source(ctx, proxy, orientation);
session.full = Some(Arc::new(raw));
Ok(session)
}
/// Prepare an edit graph over an already-processed RGB image.
///
/// The JPEG path. A JPEG is already demosaiced, so there is no sensor
@@ -379,6 +431,8 @@ impl DevelopSession {
compared_snapshot: None,
crop_notice: None,
demosaiced: Arc::new(demosaiced),
full: None,
window: None,
adjust: AdjustPass::new(ctx),
histogram: HistogramPass::new(ctx)
.inspect_err(|e| log::warn!("no histogram on this device: {e}"))