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:
@@ -83,6 +83,13 @@ impl ParamCapability {
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DSP-2
|
||||
/// How far past the framing's own footprint [`EditGraph::source_region`]
|
||||
/// reaches when a lens warp is active, as a fraction of the frame on each
|
||||
/// side. Distortion profiles move a corner by a few per cent of the frame; a
|
||||
/// window short of what the warp reads would render the missing strip black.
|
||||
pub const WARP_MARGIN: f32 = 0.04;
|
||||
|
||||
/// An ordered pipeline of operations, plus how the result is framed.
|
||||
pub struct EditGraph {
|
||||
ops: Vec<Box<dyn Operation>>,
|
||||
@@ -292,6 +299,57 @@ impl EditGraph {
|
||||
self.framing.output_size(width, height)
|
||||
}
|
||||
|
||||
/// TRACES: FR-DSP-2 | NFR-RES-2
|
||||
/// The part of the source the visible region reads, as a rectangle in
|
||||
/// normalised source coordinates, clamped to the frame.
|
||||
///
|
||||
/// For a photograph larger than one texture: a render of part of it —
|
||||
/// the canvas zoomed in, one tile of an export — binds only this window
|
||||
/// of the source (see `dr_pipeline::SOURCE_WINDOW_UNIFORM_FIELDS`).
|
||||
///
|
||||
/// The framing is walked on the CPU with [`Framing::source_at`], along
|
||||
/// the border and across the interior, so a straightened or keystoned
|
||||
/// view gets the box around the quadrilateral it actually reads. The lens
|
||||
/// warps have no CPU mirror, so when one is active the box is widened
|
||||
/// by [`WARP_MARGIN`] of the frame on each side: a distortion profile
|
||||
/// moves a corner by a few per cent of the frame at most. `halo`, in
|
||||
/// source pixels, is added on top — the detail stage's reach, which reads
|
||||
/// beyond the pixels it writes.
|
||||
pub fn source_region(&self, source: (u32, u32), halo: u32) -> crate::framing::CropRect {
|
||||
const STEPS: usize = 16;
|
||||
let (sw, sh) = (source.0.max(1), source.1.max(1));
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
||||
for j in 0..=STEPS {
|
||||
for i in 0..=STEPS {
|
||||
let out = (i as f32 / STEPS as f32, j as f32 / STEPS as f32);
|
||||
let (x, y) = self.framing.source_at(out, sw, sh);
|
||||
x0 = x0.min(x);
|
||||
y0 = y0.min(y);
|
||||
x1 = x1.max(x);
|
||||
y1 = y1.max(y);
|
||||
}
|
||||
}
|
||||
let warp = if crate::lens::compose_warps(&self.warps).is_active() {
|
||||
WARP_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Two pixels beyond the halo: the bilinear tap's second texel, and
|
||||
// the rounding of the box to whole pixels by the caller.
|
||||
let px = (halo as f32 + 2.0) / sw as f32;
|
||||
let py = (halo as f32 + 2.0) / sh as f32;
|
||||
let x0 = (x0 - warp - px).clamp(0.0, 1.0);
|
||||
let y0 = (y0 - warp - py).clamp(0.0, 1.0);
|
||||
let x1 = (x1 + warp + px).clamp(0.0, 1.0);
|
||||
let y1 = (y1 + warp + py).clamp(0.0, 1.0);
|
||||
crate::framing::CropRect {
|
||||
x: x0,
|
||||
y: y0,
|
||||
width: (x1 - x0).max(0.0),
|
||||
height: (y1 - y0).max(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Descriptors for every operation, in order.
|
||||
///
|
||||
/// Operations only — framing is not one, and is reached through
|
||||
|
||||
@@ -50,6 +50,7 @@ pub mod preset;
|
||||
pub mod sidecar;
|
||||
pub mod spot;
|
||||
pub mod state;
|
||||
pub mod tiles;
|
||||
pub mod view;
|
||||
|
||||
pub use coverage::Coverage;
|
||||
|
||||
@@ -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