//! TRACES: FR-DEV-3e //! The camera profile's base curve, end to end on a device. //! //! The unit tests either side of this one check halves. `dr-decode` asserts //! that the shipped database parses and that every curve in it lifts its //! midtones; `dr-pipeline` asserts that the generated WGSL evaluates a curve //! in the right place. Neither would notice if the two agreed with each other //! and both were wrong — a curve packed into the wrong uniform slots, or a //! flag read from the wrong component, satisfies both and renders nothing. //! //! So this renders real pixels twice, once with a profiled body's curve and //! once with the identity, and asserts the difference is the one a base curve //! is for: midtones lifted, black still black, white still white. use dr_decode::{BaseCurve, CfaPattern, CropRect, RawImage}; use dr_gpu::{AdjustPass, Demosaicer, GpuContext}; use dr_pipeline::EditGraph; const SIZE: u32 = 16; fn ctx() -> Option { pollster::block_on(GpuContext::new_headless()).ok() } /// A flat RGGB frame at `level` out of 65535, carrying `curve`. /// /// Every photosite the same value, so the demosaic result is a uniform grey /// and the only thing that can move a pixel is the curve. The colour matrix is /// the identity and the balance is neutral for the same reason: this test is /// about one stage, and a real body's matrix would make every assertion below /// a statement about that body instead. fn flat_raw(level: u16, curve: BaseCurve) -> 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]), base_curve: curve, crop: CropRect { x: 0, y: 0, width: SIZE, height: SIZE, }, } } /// Render a neutral edit over a flat frame and return the centre pixel's red. /// /// The centre rather than a corner: a demosaic has to invent its edges, and /// the interpolated border of a 16×16 frame is not where anyone should be /// reading a tone off. fn rendered_level(ctx: &GpuContext, level: u16, curve: BaseCurve) -> u8 { let raw = flat_raw(level, curve); let source = Demosaicer::new(ctx) .expect("demosaicer") .run(&raw) .expect("demosaic"); let shader = EditGraph::default_chain().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] } /// The Canon EOS 6D's curve, from the shipped profile database. /// /// Looked up by name rather than written out, so this also asserts the thing /// no other test can: that a curve travels from the YAML, through the body /// match, onto the decoded image and into the uniform block that the shader /// actually reads. fn six_d() -> BaseCurve { let curve = dr_decode::base_curve::for_body("Canon", "EOS 6D"); assert!( !curve.is_identity(), "the shipped database must have a curve for the EOS 6D" ); curve } #[test] fn a_profiled_body_renders_brighter_midtones_than_a_flat_one() { // **The whole requirement, in one assertion.** A linear midtone renders // roughly half a stop dark, which is the flat, lifeless look FR-DEV-3e // exists to get away from. If the curve did not reach the shader — wrong // slot, wrong flag, wrong stage — this is the only test that would fail. let Some(ctx) = ctx() else { eprintln!("skipping: no GPU adapter"); return; }; // 13% of full scale: roughly where a camera places middle grey, leaving // about two and a half stops of highlight headroom above it. let level = (0.13 * 65535.0) as u16; let flat = rendered_level(&ctx, level, BaseCurve::IDENTITY); let profiled = rendered_level(&ctx, level, six_d()); assert!( profiled > flat + 8, "the profile lifted middle grey from {flat} only to {profiled}" ); } #[test] fn the_curve_leaves_black_black_and_white_white() { // A base curve renders the range between the endpoints; it must not move // the endpoints themselves. A curve that lifted black would put a grey // veil over every night photograph, and one that pulled white down would // make a correctly exposed frame look underexposed. let Some(ctx) = ctx() else { eprintln!("skipping: no GPU adapter"); return; }; let curve = six_d(); assert_eq!(rendered_level(&ctx, 0, curve), 0, "black moved"); assert_eq!(rendered_level(&ctx, u16::MAX, curve), 255, "white moved"); } #[test] fn an_unprofiled_body_renders_exactly_as_it_did_before_profiles_existed() { // The graceful fallback, asserted as a number rather than as a promise. // With no curve the pipeline must still be a pass-through: black level // out, white level in, sRGB encoding on the way to the screen and nothing // else. "Never worse than today" is the one property this change was not // allowed to trade away, and the way it would break is silently — a flag // read from the wrong component would apply a curve nobody asked for. let Some(ctx) = ctx() else { eprintln!("skipping: no GPU adapter"); return; }; for level in [0u16, 4_000, 8_520, 32_768, 60_000, u16::MAX] { let scene = f32::from(level) / f32::from(u16::MAX); let expected = (dr_types::Transfer::Srgb.encode(scene) * 255.0).round() as i32; let got = i32::from(rendered_level(&ctx, level, BaseCurve::IDENTITY)); // Two 8-bit steps: the texture holding the demosaiced frame is // `Rgba16Float`, so a value round-trips through eleven mantissa bits // before it is encoded. That is well under one step at any level, and // the tolerance is for the rounding either side of it rather than for // the transform being approximate. assert!( (got - expected).abs() <= 2, "raw {level} rendered as {got}, expected about {expected}" ); } } #[test] fn the_curve_is_monotone_through_the_whole_range() { // The property the spline's tangent limiting exists to guarantee, checked // where it actually matters: on the device, through the real uniform // packing. A curve that dipped anywhere would put a dark band across a // smooth gradient — a sky, most visibly — and it would read as a // rendering fault rather than as a bad profile. let Some(ctx) = ctx() else { eprintln!("skipping: no GPU adapter"); return; }; let curve = six_d(); let mut previous = 0u8; for step in 0..=16u32 { let level = (step * 65535 / 16) as u16; let value = rendered_level(&ctx, level, curve); assert!( value >= previous, "the curve fell from {previous} to {value} at raw level {level}" ); previous = value; } }