//! 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> { 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); } }