Render raws through Camera Raw's tone by default, after baseline exposure

The rendering half of camera-profiles.md §11-§12 (D21). The view
transform gains a curve choice — Camera Raw (the default) or D19's
sigmoid. Camera Raw converts to linear ProPhoto, clips to [0, 1], runs
the curve on the largest and smallest channel and places the middle
one at its old fraction between them (RefBaselineRGBTone), and
converts back: hue kept, saturation raised where the curve is steep,
which is where Adobe Standard's look desaturated. White sets the input
scale (1 at its default, so sensor white is display white) and
contrast bends the input about grey (1 at its default).

The curve rides in the profile buffer after the tables: the profile's
own, else the ACR3 default, which the placeholder every profile-less
source binds also carries — so a CR2 with no .dcp still gets Camera
Raw's tone. Baseline exposure is a gain folded into the rendering
matrix at upload; RawImage::color_matrix stays the file's for the
merge's linear DNG.

camera_raw::apply_reference is the CPU statement; GPU tests hold the
shader to it on 256 colours and on greys against the ACR3 table. The
sigmoid's own tests now choose it explicitly.
This commit is contained in:
2026-10-03 14:22:47 -04:00
parent 38d414912c
commit db7593f3dc
9 changed files with 383 additions and 45 deletions
+36 -2
View File
@@ -46,6 +46,40 @@ fn flat_raw(level: u16) -> RawImage {
}
}
/// The default chain with D19's sigmoid chosen: Camera Raw's tone is the
/// default since D21, and these tests are about the sigmoid.
fn sigmoid_chain() -> EditGraph {
let mut g = EditGraph::default_chain();
g.set_param(
dr_pipeline::ops::view_transform::ID,
dr_pipeline::ops::view_transform::CURVE,
dr_pipeline::ops::view_transform::SIGMOID,
);
g
}
#[test]
fn camera_raw_tone_is_the_default_and_agrees_with_the_acr3_curve() {
// TRACES: FR-DEV-3j
// D21: a raw with no profile renders a grey through Camera Raw's default
// curve, which the profile buffer's placeholder carries.
let Some(ctx) = ctx() else {
eprintln!("skipping: no GPU adapter");
return;
};
let graph = EditGraph::default_chain();
for level in [500u16, 4_000, 8_520, 20_000, 40_000] {
let scene = f32::from(level) / f32::from(u16::MAX);
let display = dr_types::tone::evaluate(&dr_types::tone::ACR3_DEFAULT, scene);
let expected = (dr_types::Transfer::Srgb.encode(display) * 255.0).round() as i32;
let got = i32::from(rendered(&ctx, level, &graph));
assert!(
(got - expected).abs() <= 2,
"raw {level} rendered as {got}, the ACR3 curve says {expected}"
);
}
}
/// 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.
@@ -70,7 +104,7 @@ fn the_shader_agrees_with_the_cpu_reference() {
return;
};
let curve = Sigmoid::default_curve();
let graph = EditGraph::default_chain();
let graph = sigmoid_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);
@@ -96,7 +130,7 @@ fn highlights_above_one_stay_distinct() {
eprintln!("skipping: no GPU adapter");
return;
};
let mut graph = EditGraph::default_chain();
let mut graph = sigmoid_chain();
graph.set_param(
dr_pipeline::ops::exposure::ID,
dr_pipeline::ops::exposure::EXPOSURE,