Merge branch 'colour-managed-export'

This commit is contained in:
2026-08-17 09:28:22 +02:00
11 changed files with 1672 additions and 66 deletions
+78
View File
@@ -1260,6 +1260,84 @@ mod tests {
);
}
#[test]
fn every_output_colour_space_renders_what_the_colorimetry_predicts() {
// TRACES: FR-EXP-2
// The shader carries constants generated from `dr_types::colour`; this
// recomputes the same conversion on the CPU and demands the GPU agree.
// A transposed matrix, a transfer function applied before the
// primaries, or a clip in the wrong place all compile perfectly and
// simply produce the wrong colour — none of which a "did it compile"
// test would notice.
//
// A saturated patch, deliberately: every one of these spaces maps a
// neutral to itself, so a grey would agree with all four.
let Some(ctx) = ctx() else { return };
let mut pass = AdjustPass::new(&ctx);
let source = [200u8, 90, 40];
let img = jpeg_image(&ctx, source);
// The JPEG path linearises with the sRGB curve, so this is the value
// reaching the output stage.
let linear: Vec<f32> = source
.iter()
.map(|&v| dr_types::Transfer::Srgb.decode(f32::from(v) / 255.0))
.collect();
for space in dr_types::ColourSpace::ALL {
let shader = EditGraph::default_chain().compose_for(space);
let t = pass.render(&img, &shader, 16, 16).expect("render");
let got = read_centre(&ctx, t);
let m = space.from_linear_srgb();
for channel in 0..3 {
let converted = m[channel * 3] * linear[0]
+ m[channel * 3 + 1] * linear[1]
+ m[channel * 3 + 2] * linear[2];
let want = space.transfer().encode(converted.clamp(0.0, 1.0)) * 255.0;
let delta = (f32::from(got[channel]) - want).abs();
// Two levels: the pipeline stores its intermediate in f16 and
// the source itself came from an 8-bit texel, so exactness is
// not on offer. A wrong matrix is out by tens.
assert!(
delta <= 2.0,
"{space:?} channel {channel}: rendered {} against a predicted {want:.1} \
(whole pixel {got:?})",
got[channel]
);
}
}
}
#[test]
fn a_wide_gamut_render_differs_from_an_srgb_one() {
// The companion to the test above, and the one that would fail if the
// output space were accepted and then ignored: predicted values that
// happened to match sRGB's would prove nothing. A saturated red is
// several tens of levels apart in P3.
let Some(ctx) = ctx() else { return };
let mut pass = AdjustPass::new(&ctx);
let img = jpeg_image(&ctx, [230, 30, 20]);
let srgb = {
let shader = EditGraph::default_chain().compose_for(dr_types::ColourSpace::Srgb);
let t = pass.render(&img, &shader, 16, 16).expect("render");
read_centre(&ctx, t)
};
let p3 = {
let shader = EditGraph::default_chain().compose_for(dr_types::ColourSpace::DisplayP3);
let t = pass.render(&img, &shader, 16, 16).expect("render");
read_centre(&ctx, t)
};
// Less red and more green: the same colour expressed against wider
// primaries needs smaller numbers to reach it.
assert!(
p3[0] < srgb[0] && p3[1] > srgb[1],
"sRGB rendered {srgb:?} and Display P3 {p3:?}"
);
}
#[test]
fn a_jpeg_and_sensor_data_agree_on_the_same_scene_value() {
// The two producers must be interchangeable. A mid-grey that is