The fused pass stops at "linear working values" when a sharpener, a blur or a repair follows, and the detail passes convolve what it hands on. Until now it handed on the rendering: the base curve, and since the last commit the view transform, ran before the store. So every kernel worked on display-referred values while its comments promised the opposite — D19's second finding. A fused pass composed for a detail stage now stops before the view transform, and carries a second shader, `ComposedShader::view`, composed from the same inputs. It runs the same prologue, for the positions a fragment reads (a film's grain seeds from `source_px`) and the corners it blacks out, takes its colour from the detail stage's result bound where the sample cache would be, and runs the view transform, the output transform and the mask reveal. `render_detailed` dispatches it after the last detail pass, in the same encoder. So no detail pass encodes any more. Every pass writes an intermediate, the last one included, which retires three things that existed only to make the last pass encode: `writes_output` and the runner's second layout, the body-less resolve pass for an active kernel with nothing to draw at this scale, and capture sharpening's pass-through, which now emits no pass at all. An empty chain is a whole render: the view pass reads the fused result directly. The detail stage no longer takes an output space either, so `compose_detail_for` folds into `compose_detail` and the space is named once, on the fused half. The cost is one full-render read and write per frame when a detail stage exists, and a third intermediate for a one-pass chain.
179 lines
6.5 KiB
Rust
179 lines
6.5 KiB
Rust
//! TRACES: FR-DEV-3j | FR-DEV-2
|
|
//! The view transform, end to end on a device.
|
|
//!
|
|
//! `dr-pipeline` checks the curve on the CPU and that the composer emits it in
|
|
//! the right place. Neither would notice a shader that disagreed with the CPU
|
|
//! reference, or a clamp somewhere upstream that made two highlights the same
|
|
//! number before the curve ever saw them — which is exactly what the retired
|
|
//! base curve did, and why D19 exists. So this renders real pixels.
|
|
|
|
use dr_decode::{CfaPattern, CropRect, RawImage};
|
|
use dr_gpu::{AdjustPass, Demosaicer, GpuContext};
|
|
use dr_pipeline::view::Sigmoid;
|
|
use dr_pipeline::EditGraph;
|
|
|
|
const SIZE: u32 = 16;
|
|
|
|
fn ctx() -> Option<GpuContext> {
|
|
pollster::block_on(GpuContext::new_headless()).ok()
|
|
}
|
|
|
|
/// A flat RGGB frame at `level` out of 65535, with an identity matrix and a
|
|
/// neutral balance, so the only things that move a pixel are the edit and the
|
|
/// view transform.
|
|
fn flat_raw(level: u16) -> RawImage {
|
|
RawImage {
|
|
width: SIZE,
|
|
height: SIZE,
|
|
data: vec![level; (SIZE * SIZE) as usize],
|
|
cfa_pattern: CfaPattern::Rggb,
|
|
black_level: [0; 4],
|
|
white_level: u16::MAX,
|
|
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
|
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
|
samples_per_pixel: 1,
|
|
profile: None,
|
|
make: String::new(),
|
|
model: String::new(),
|
|
crop: CropRect {
|
|
x: 0,
|
|
y: 0,
|
|
width: SIZE,
|
|
height: SIZE,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Render `graph` over a flat frame and return the centre pixel's red.
|
|
///
|
|
/// The centre rather than a corner: a demosaic has to invent its edges.
|
|
fn rendered(ctx: &GpuContext, level: u16, graph: &EditGraph) -> u8 {
|
|
let source = Demosaicer::new(ctx)
|
|
.expect("demosaicer")
|
|
.run(&flat_raw(level))
|
|
.expect("demosaic");
|
|
let shader = graph.compose();
|
|
let mut adjust = AdjustPass::new(ctx);
|
|
adjust.render(&source, &shader, SIZE, SIZE).expect("render");
|
|
let (pixels, _, _) = adjust.export_pixels().expect("readback");
|
|
let centre = ((SIZE / 2) * SIZE + SIZE / 2) * 4;
|
|
pixels[centre as usize]
|
|
}
|
|
|
|
#[test]
|
|
fn the_shader_agrees_with_the_cpu_reference() {
|
|
// TRACES: FR-DEV-3j
|
|
let Some(ctx) = ctx() else {
|
|
eprintln!("skipping: no GPU adapter");
|
|
return;
|
|
};
|
|
let curve = Sigmoid::default_curve();
|
|
let graph = EditGraph::default_chain();
|
|
for level in [0u16, 500, 4_000, 8_520, 32_768, 60_000, u16::MAX] {
|
|
let scene = f32::from(level) / f32::from(u16::MAX);
|
|
let display = curve.channel(scene).min(1.0);
|
|
let expected = (dr_types::Transfer::Srgb.encode(display) * 255.0).round() as i32;
|
|
let got = i32::from(rendered(&ctx, level, &graph));
|
|
// Two 8-bit steps, for the `Rgba16Float` intermediate and the
|
|
// rounding either side of the encode.
|
|
assert!(
|
|
(got - expected).abs() <= 2,
|
|
"raw {level} rendered as {got}, expected about {expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn highlights_above_one_stay_distinct() {
|
|
// TRACES: FR-DEV-2 | FR-DEV-3j
|
|
// The failure D19 names first. Two stops of exposure put these two
|
|
// frames at 1.0 and 1.5 of sensor saturation. The base curve was flat
|
|
// past 1.0, so both rendered as the same white; the view transform's
|
|
// shoulder still separates them.
|
|
let Some(ctx) = ctx() else {
|
|
eprintln!("skipping: no GPU adapter");
|
|
return;
|
|
};
|
|
let mut graph = EditGraph::default_chain();
|
|
graph.set_param(
|
|
dr_pipeline::ops::exposure::ID,
|
|
dr_pipeline::ops::exposure::EXPOSURE,
|
|
2.0,
|
|
);
|
|
let lower = rendered(&ctx, u16::MAX / 4, &graph);
|
|
let upper = rendered(&ctx, (u16::MAX / 8) * 3, &graph);
|
|
assert!(
|
|
upper > lower,
|
|
"scene 1.0 rendered {lower} and scene 1.5 rendered {upper}"
|
|
);
|
|
assert!(upper < 255, "scene 1.5 is below the default white point");
|
|
}
|
|
|
|
#[test]
|
|
fn the_rendering_is_monotone_through_the_whole_range() {
|
|
// TRACES: FR-DEV-3j
|
|
// A dip anywhere puts a dark band across a smooth gradient — a sky, most
|
|
// visibly.
|
|
let Some(ctx) = ctx() else {
|
|
eprintln!("skipping: no GPU adapter");
|
|
return;
|
|
};
|
|
let graph = EditGraph::default_chain();
|
|
let mut last = 0u8;
|
|
for step in 0..=32u32 {
|
|
let level = (step * u32::from(u16::MAX) / 32) as u16;
|
|
let got = rendered(&ctx, level, &graph);
|
|
assert!(got >= last, "raw {level} rendered {got}, below {last}");
|
|
last = got;
|
|
}
|
|
}
|
|
|
|
/// Render `graph` over a flat frame through `render_detailed`, the path every
|
|
/// frontend takes, and return the centre pixel's red.
|
|
fn rendered_detailed(ctx: &GpuContext, level: u16, graph: &EditGraph) -> (u8, AdjustPass) {
|
|
let source = Demosaicer::new(ctx)
|
|
.expect("demosaicer")
|
|
.run(&flat_raw(level))
|
|
.expect("demosaic");
|
|
let shader = graph.compose_for(dr_types::ColourSpace::Srgb);
|
|
let detail = graph.compose_detail(source.size(), (SIZE, SIZE));
|
|
let key = graph.invalidation().through(dr_pipeline::Affects::Colour);
|
|
let mut adjust = AdjustPass::new(ctx);
|
|
adjust
|
|
.render_detailed(&source, &shader, SIZE, SIZE, None, &detail, key)
|
|
.expect("render");
|
|
let (pixels, _, _) = adjust.export_pixels().expect("readback");
|
|
let centre = ((SIZE / 2) * SIZE + SIZE / 2) * 4;
|
|
(pixels[centre as usize], adjust)
|
|
}
|
|
|
|
#[test]
|
|
fn a_detail_stage_renders_through_the_view_pass_unchanged() {
|
|
// TRACES: FR-DEV-3j | FR-DEV-2
|
|
// With a detail stage the view transform is a dispatch of its own after
|
|
// it (D19). Sharpening a flat field changes nothing, so the same frame
|
|
// with and without it must render the same: the view pass read the detail
|
|
// stage's result, applied the view transform once, and encoded once.
|
|
let Some(ctx) = ctx() else {
|
|
eprintln!("skipping: no GPU adapter");
|
|
return;
|
|
};
|
|
let plain = rendered(&ctx, 8_520, &EditGraph::default_chain());
|
|
|
|
let mut sharpened = EditGraph::default_chain();
|
|
let id = dr_pipeline::ops::capture_sharpen::ID;
|
|
sharpened.set_param(id, dr_pipeline::ops::capture_sharpen::AMOUNT, 100.0);
|
|
sharpened.set_param(id, dr_pipeline::ops::capture_sharpen::RADIUS, 1.0);
|
|
let (detailed, pass) = rendered_detailed(&ctx, 8_520, &sharpened);
|
|
|
|
assert!(
|
|
pass.detail_dispatches() > 0,
|
|
"the premise: a detail stage ran"
|
|
);
|
|
assert_eq!(pass.view_dispatches(), 1);
|
|
assert!(
|
|
detailed.abs_diff(plain) <= 1,
|
|
"with a detail stage {detailed}, without {plain}"
|
|
);
|
|
}
|