The test demanded a full halo on every side except the frame's own edge, but a tile one step in from it is grown to the edge and no further, exactly where the untiled render stops too.
156 lines
5.9 KiB
Rust
156 lines
5.9 KiB
Rust
//! 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, or the frame ends first — in
|
||
// which case the untiled render stops at the same edge.
|
||
assert!(gx == 0 || kx - gx >= halo);
|
||
assert!(gy == 0 || ky - gy >= halo);
|
||
assert!(gx + gw == frame.0 || gx + gw - (kx + kw) >= halo);
|
||
assert!(gy + gh == 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);
|
||
}
|
||
}
|