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:
@@ -0,0 +1,208 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user