//! TRACES: FR-DSP-2 | NFR-RES-2 //! A photograph larger than one texture, developed from windows of it. //! //! The claim under test is that the window is invisible: a frame rendered a //! tile at a time, each tile from only the part of the source it reads, is the //! frame rendered whole. `dr-pipeline` can check the plan — the tiles cover //! the frame once, each is grown by the reach — but not that the shader's //! mapping into a window lands on the texel the whole texture would have //! given, which only a device answers. //! //! The frames here are small and the "device limit" is a number passed in, //! so the tiling is exercised on any adapter, including one whose real limit //! a test image could never approach. use dr_decode::{CfaPattern, CropRect, RawImage}; use dr_gpu::{AdjustPass, DemosaicedImage, GpuContext}; use dr_pipeline::descriptor::{OpId, ParamId}; use dr_pipeline::framing::ANGLE; use dr_pipeline::{tiles, Affects, EditGraph}; use dr_types::ColourSpace; fn ctx() -> Option { match pollster::block_on(GpuContext::new_headless()) { Ok(c) => Some(c), Err(e) => { eprintln!("skipping: no GPU adapter ({e})"); None } } } /// A linear RGB frame with detail at every scale: a slow gradient for the /// tone controls and a hash for the kernels, so a tile that read one pixel /// off would show. fn linear_frame(w: u32, h: u32, noise: bool) -> RawImage { let mut data = Vec::with_capacity((w * h * 3) as usize); for y in 0..h { for x in 0..w { let base = 4000.0 + 30000.0 * (x as f32 / w as f32) + 12000.0 * (y as f32 / h as f32); let hash = if noise { ((x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)) % 8000) as f32 } else { 0.0 }; for c in 0..3 { data.push((base * (0.7 + 0.15 * c as f32) + hash) as u16); } } } RawImage { width: w, height: h, data, cfa_pattern: CfaPattern::Unknown, black_level: [512; 4], white_level: 65535, wb_coeffs: [2.0, 1.0, 1.5, 1.0], color_matrix: Some([1.6, -0.5, -0.1, -0.2, 1.4, -0.2, 0.0, -0.4, 1.4]), samples_per_pixel: 3, profile: None, profile_tables: None, baseline_exposure: 0.0, make: String::new(), model: String::new(), crop: CropRect { x: 0, y: 0, width: w, height: h, }, } } /// Render `graph` over `source` at `size` and read it back. fn render( pass: &mut AdjustPass, graph: &EditGraph, source: &DemosaicedImage, size: (u32, u32), ) -> Vec { let shader = graph.compose_for(ColourSpace::Srgb); let detail = graph.compose_detail(source.size(), size); let key = graph.invalidation().through(Affects::Colour); pass.render_detailed(source, &shader, size.0, size.1, None, &detail, key) .expect("render"); pass.export_pixels().expect("readback").0 } /// The frame at full resolution, a tile at a time, each from its own window. fn render_tiled( ctx: &GpuContext, pass: &mut AdjustPass, graph: &mut EditGraph, raw: &RawImage, max_edge: u32, ) -> (Vec, usize) { let frame = (raw.crop.width, raw.crop.height); let out = graph.output_size(frame.0, frame.1); let reach = graph.compose_detail(frame, out).reach(); let plan = tiles::plan(out, max_edge, reach).expect("a plan"); let mut pixels = vec![0u8; (out.0 * out.1 * 4) as usize]; for t in &plan { graph.framing_mut().set_view(t.view(out)); let r = graph.source_region(frame, 0); let x0 = (r.x * frame.0 as f32).floor() as u32; let y0 = (r.y * frame.1 as f32).floor() as u32; let x1 = ((r.x + r.width) * frame.0 as f32).ceil() as u32; let y1 = ((r.y + r.height) * frame.1 as f32).ceil() as u32; let window = DemosaicedImage::linear_rgb16_window(ctx, raw, [x0, y0, x1 - x0, y1 - y0], 1) .expect("window"); assert_eq!(window.size(), frame, "a window measures the frame"); let tile = render(pass, graph, &window, (t.grown[2], t.grown[3])); let (ox, oy) = t.keep_offset(); for row in 0..t.keep[3] { let src = (((oy + row) * t.grown[2] + ox) * 4) as usize; let dst = (((t.keep[1] + row) * out.0 + t.keep[0]) * 4) as usize; let n = (t.keep[2] * 4) as usize; pixels[dst..dst + n].copy_from_slice(&tile[src..src + n]); } } graph .framing_mut() .set_view(dr_pipeline::CropRect::default()); (pixels, plan.len()) } fn largest_difference(a: &[u8], b: &[u8]) -> u8 { a.iter() .zip(b) .map(|(x, y)| x.abs_diff(*y)) .max() .unwrap_or(0) } #[test] fn tiles_of_windows_are_the_whole_frame() { // Point operations only, unrotated: every output pixel is an exact load // of one source texel, so the tiled frame has to be the whole one to // the bit. let Some(ctx) = ctx() else { return }; let raw = linear_frame(200, 120, true); let mut graph = EditGraph::default_chain(); graph.set_param(OpId("exposure"), ParamId("exposure"), 0.7); let mut pass = AdjustPass::new(&ctx); let whole = DemosaicedImage::from_linear_rgb16(&ctx, &raw).unwrap(); assert!(whole.is_whole()); let reference = render(&mut pass, &graph, &whole, (200, 120)); let (tiled, n) = render_tiled(&ctx, &mut pass, &mut graph, &raw, 64); assert!(n > 4, "the frame should have been cut, got {n} tile(s)"); assert_eq!(largest_difference(&reference, &tiled), 0); } #[test] fn a_straightened_frame_with_clarity_tiles_without_seams() { // The hard case: a free angle samples between texels, and clarity reads // a wide neighbourhood on a reduced grid. The halo and the grid // alignment are what keep the tiles' edges out of the picture; a code // value of rounding is all that may differ. let Some(ctx) = ctx() else { return }; let raw = linear_frame(320, 208, true); let mut graph = EditGraph::default_chain(); graph.set_param(OpId("clarity"), ParamId("amount"), 60.0); graph.framing_mut().set_param(ANGLE, 3.0); let mut pass = AdjustPass::new(&ctx); let whole = DemosaicedImage::from_linear_rgb16(&ctx, &raw).unwrap(); let out = graph.output_size(320, 208); let reference = render(&mut pass, &graph, &whole, out); let (tiled, n) = render_tiled(&ctx, &mut pass, &mut graph, &raw, 160); assert!(n > 1, "the frame should have been cut, got {n} tile(s)"); let worst = largest_difference(&reference, &tiled); assert!( worst <= 1, "tiles differ from the whole frame by {worst} code values" ); } #[test] fn a_reduced_copy_stands_for_the_whole_frame() { // The canvas at fit renders from a copy reduced to fit the device. It // must measure the photograph, not itself, or a crop drawn on it lands // somewhere else in the export; and rendered small it must look like the // full frame rendered small. let Some(ctx) = ctx() else { return }; let raw = linear_frame(400, 240, false); let mut graph = EditGraph::default_chain(); graph.set_crop(dr_pipeline::CropRect { x: 0.25, y: 0.1, width: 0.5, height: 0.6, }); let mut pass = AdjustPass::new(&ctx); let whole = DemosaicedImage::from_linear_rgb16(&ctx, &raw).unwrap(); let reduced = DemosaicedImage::linear_rgb16_window(&ctx, &raw, [0, 0, 400, 240], 3).unwrap(); assert_eq!(reduced.size(), (400, 240)); assert_eq!(reduced.texture_size(), (134, 80)); assert!(!reduced.is_whole()); let size = (50, 36); let a = render(&mut pass, &graph, &whole, size); let b = render(&mut pass, &graph, &reduced, size); let worst = largest_difference(&a, &b); assert!( worst <= 3, "the reduced copy renders {worst} code values away" ); }