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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user