Develop a linear DNG from windows and reduced copies of it

DemosaicedImage::linear_rgb16_window uploads part of a linear DNG, or a
box-reduced copy of it, and says where it sits in the frame; size() now
reports the frame and texture_size() the texels, and the fused pass
writes the window into the shader's uniforms. EditGraph::source_region
finds the part of the source a view reads, and tiles::plan cuts a render
too large for one texture into halo-grown, grid-aligned tiles.

The GPU test renders frames a tile at a time from their own windows and
compares them with the whole: identical for point operations, within one
code value when straightened with clarity on.
This commit is contained in:
2026-09-27 17:35:16 -04:00
parent 1eab723c90
commit 0007fa459f
9 changed files with 600 additions and 38 deletions
+154
View File
@@ -0,0 +1,154 @@
//! TRACES: FR-DSP-2 | NFR-RES-2
//! Cutting a render too large for one texture into tiles.
//!
//! The interactive path is not tiled, and on the evidence should not be
//! (`docs/dev/frame-budget.md`, TD-4): one fused dispatch over a viewport is
//! inside the frame budget, and a halo per tile nearly doubles the taps of a
//! wide kernel. What does not fit is a *file*. A 22927×8966 panorama has no
//! render target on a device whose textures stop at 16384, so its export, and
//! nothing else, is drawn a tile at a time.
//!
//! A tile is two rectangles in pixels of the framed output: the one rendered,
//! grown by the detail stage's reach ([`crate::ComposedDetail::reach`]) so
//! every kernel near its edge reads the pixels it would read untiled, and the
//! one kept, which is the tile proper. The kept rectangles cover the frame
//! exactly once.
//!
//! The rendered rectangle's origin is aligned to [`TILE_ALIGN`]. The detail
//! stage computes clarity's base on a reduced grid, and a tile starting half
//! way through a reduced texel would reduce different pixels together than
//! the untiled frame does, which shows as a faint seam.
/// A multiple of every reduced grid the detail stage uses, so a tile's
/// grids line up with the untiled frame's.
pub const TILE_ALIGN: u32 = 16;
/// One tile of a render: what to draw, and which part of it to keep.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tile {
/// `[x, y, width, height]` in output pixels: the tile grown by the halo,
/// clamped to the frame. This is what is rendered.
pub grown: [u32; 4],
/// `[x, y, width, height]` in output pixels: the tile proper, which lies
/// inside `grown`. This is what is kept.
pub keep: [u32; 4],
}
impl Tile {
/// The rendered rectangle as a view on the frame, the rectangle
/// [`crate::Framing::set_view`] takes.
pub fn view(&self, frame: (u32, u32)) -> crate::framing::CropRect {
let (fw, fh) = (frame.0.max(1) as f32, frame.1.max(1) as f32);
crate::framing::CropRect {
x: self.grown[0] as f32 / fw,
y: self.grown[1] as f32 / fh,
width: self.grown[2] as f32 / fw,
height: self.grown[3] as f32 / fh,
}
}
/// Where the kept rectangle starts inside the rendered one.
pub fn keep_offset(&self) -> (u32, u32) {
(self.keep[0] - self.grown[0], self.keep[1] - self.grown[1])
}
}
/// Cut a `frame`-sized render into tiles no larger than `max_edge` once
/// grown by `halo` on every side.
///
/// Row-major, top to bottom, so a caller writing the file as it goes gets
/// its bands in order. A frame that fits whole is one tile with no halo.
/// `None` when the halo leaves no room for a tile at all — a spot heal
/// cloning from across a frame wider than the device can hold is the case,
/// and it has to be refused rather than drawn with a seam.
pub fn plan(frame: (u32, u32), max_edge: u32, halo: u32) -> Option<Vec<Tile>> {
let (fw, fh) = (frame.0.max(1), frame.1.max(1));
if fw <= max_edge && fh <= max_edge {
return Some(vec![Tile {
grown: [0, 0, fw, fh],
keep: [0, 0, fw, fh],
}]);
}
// The halo, rounded up so a grown origin lands on the grid; the tile
// proper a multiple of it for the same reason.
let halo = halo.div_ceil(TILE_ALIGN) * TILE_ALIGN;
let room = max_edge.checked_sub(2 * halo)?;
let step = room / TILE_ALIGN * TILE_ALIGN;
if step == 0 {
return None;
}
let mut out = Vec::new();
let mut y = 0;
while y < fh {
let kh = step.min(fh - y);
let mut x = 0;
while x < fw {
let kw = step.min(fw - x);
let gx = x.saturating_sub(halo);
let gy = y.saturating_sub(halo);
let gx1 = (x + kw + halo).min(fw);
let gy1 = (y + kh + halo).min(fh);
out.push(Tile {
grown: [gx, gy, gx1 - gx, gy1 - gy],
keep: [x, y, kw, kh],
});
x += kw;
}
y += kh;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_frame_that_fits_is_one_tile_with_no_halo() {
let tiles = plan((6000, 4000), 8192, 200).unwrap();
assert_eq!(tiles.len(), 1);
assert_eq!(tiles[0].grown, [0, 0, 6000, 4000]);
assert_eq!(tiles[0].keep, tiles[0].grown);
}
#[test]
fn the_kept_rectangles_cover_the_frame_exactly_once() {
// The panorama that started this, against a 16384 device with a
// clarity-sized halo.
let frame = (22927, 8966);
let tiles = plan(frame, 16384, 230).unwrap();
let mut covered = vec![0u8; (frame.0 * frame.1) as usize];
for t in &tiles {
let [x, y, w, h] = t.keep;
for yy in y..y + h {
for xx in x..x + w {
covered[(yy * frame.0 + xx) as usize] += 1;
}
}
}
assert!(covered.iter().all(|&c| c == 1));
}
#[test]
fn every_tile_fits_the_device_and_holds_its_halo() {
let frame = (22927, 8966);
let (max, halo) = (8192, 300);
for t in plan(frame, max, halo).unwrap() {
let [gx, gy, gw, gh] = t.grown;
let [kx, ky, kw, kh] = t.keep;
assert!(gw <= max && gh <= max, "{t:?} does not fit");
assert_eq!(gx % TILE_ALIGN, 0, "{t:?} starts off the grid");
assert_eq!(gy % TILE_ALIGN, 0, "{t:?} starts off the grid");
// The halo is there on every side the frame does not end.
assert!(kx == 0 || kx - gx >= halo);
assert!(ky == 0 || ky - gy >= halo);
assert!(kx + kw == frame.0 || gx + gw - (kx + kw) >= halo);
assert!(ky + kh == frame.1 || gy + gh - (ky + kh) >= halo);
}
}
#[test]
fn a_halo_wider_than_the_device_is_refused() {
assert_eq!(plan((40000, 100), 16384, 9000), None);
}
}