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.
209 lines
7.8 KiB
Rust
209 lines
7.8 KiB
Rust
//! 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<GpuContext> {
|
|
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,
|
|
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<u8> {
|
|
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<u8>, 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"
|
|
);
|
|
}
|