//! TRACES: FR-DEV-3 //! The tone curve's four curves, on a device. //! //! `dr-pipeline` asserts that the right WGSL is generated and `dr-gpu`'s other //! tests assert that a shader runs; neither notices a fragment that says //! exactly what it should and does not compile, or one that compiles and puts //! the red curve's uniforms into the blue slot. So this renders flat grey //! through each curve and looks at what came out. //! //! Flat grey because it makes every assertion a comparison between the three //! components of one pixel: a curve that is meant to be chromatic must move //! them apart, and one that is meant to be tonal must not. use dr_gpu::{AdjustPass, DemosaicedImage, GpuContext}; use dr_pipeline::ops::curve::{self, Axis, Channel}; use dr_pipeline::EditGraph; const SIZE: u32 = 8; fn ctx() -> Option { pollster::block_on(GpuContext::new_headless()).ok() } /// The centre pixel's red, green and blue, after `graph` has run over flat /// mid-grey. fn rendered(ctx: &GpuContext, graph: &EditGraph) -> (u8, u8, u8) { let data: Vec = (0..SIZE * SIZE) .flat_map(|_| [128, 128, 128, 255]) .collect(); let source = DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload"); // Composed the way the display path composes it. A curve that generates // invalid WGSL fails at `render` below, which is the point of running this // on a device at all. 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").0; let at = ((SIZE / 2 * SIZE + SIZE / 2) * 4) as usize; (pixels[at], pixels[at + 1], pixels[at + 2]) } /// A curve with its mid-point lifted — the simplest edit that is unmistakably /// an edit. fn lifted(channel: Channel) -> EditGraph { let mut graph = EditGraph::default_chain(); graph.set_param(curve::ID, curve::coordinate(channel, 2, Axis::Y), 0.75); graph } #[test] fn the_master_curve_lifts_every_component_together() { let Some(ctx) = ctx() else { eprintln!("no adapter; skipping"); return; }; let (r0, g0, b0) = rendered(&ctx, &EditGraph::default_chain()); let (r, g, b) = rendered(&ctx, &lifted(Channel::Master)); assert!( r > r0, "the master curve did not lift the image: {r} vs {r0}" ); // Grey in, grey out: the master curve is applied as a ratio over // luminance, so it changes tone and not hue. A tolerance of one code // value, because the components travel through the ratio separately and // the result is quantised to eight bits. assert!( r.abs_diff(g) <= 1 && g.abs_diff(b) <= 1, "the master curve tinted a neutral pixel: {r},{g},{b}" ); assert_eq!((g0, b0), (r0, r0), "the unedited image is neutral"); } #[test] fn a_channel_curve_lifts_only_its_own_component() { let Some(ctx) = ctx() else { eprintln!("no adapter; skipping"); return; }; let (r0, g0, b0) = rendered(&ctx, &EditGraph::default_chain()); for (channel, name) in [ (Channel::Red, "red"), (Channel::Green, "green"), (Channel::Blue, "blue"), ] { let (r, g, b) = rendered(&ctx, &lifted(channel)); // The component the curve names moves; the other two stay exactly // where they were. This is what catches a fragment whose uniforms are // wired to the wrong curve — it would still lift *something*. let (moved, still) = match channel { Channel::Red => (r > r0, g == g0 && b == b0), Channel::Green => (g > g0, r == r0 && b == b0), _ => (b > b0, r == r0 && g == g0), }; assert!(moved, "the {name} curve changed nothing: {r},{g},{b}"); assert!( still, "the {name} curve moved a component that was not its own: \ {r},{g},{b} from {r0},{g0},{b0}" ); } } #[test] fn the_master_and_the_channels_compose_in_one_pass() { // All four curves at once: the case where the generated fragment is // longest, every helper is present, and forty uniforms are in the block. // Mostly a compile check, which is why the assertion is only that the // result is a colour and not the one we started with. let Some(ctx) = ctx() else { eprintln!("no adapter; skipping"); return; }; let mut graph = EditGraph::default_chain(); graph.set_param(curve::ID, curve::P1_Y, 0.15); graph.set_param(curve::ID, curve::P3_Y, 0.85); for (channel, y) in [ (Channel::Red, 0.55), (Channel::Green, 0.5), (Channel::Blue, 0.62), ] { graph.set_param(curve::ID, curve::coordinate(channel, 2, Axis::Y), y); } let (r0, _, _) = rendered(&ctx, &EditGraph::default_chain()); let (r, g, b) = rendered(&ctx, &graph); assert!( (r, g, b) != (r0, r0, r0), "four active curves left the image untouched" ); // Red and blue were pushed apart from green, which is the chromatic half // doing its work on top of the tonal one. assert!(b > g, "blue was lifted above green: {r},{g},{b}"); }