diff --git a/crates/gpu/src/wgsl/finish.wgsl b/crates/gpu/src/wgsl/finish.wgsl index 04115b945..767ff44c9 100644 --- a/crates/gpu/src/wgsl/finish.wgsl +++ b/crates/gpu/src/wgsl/finish.wgsl @@ -106,7 +106,9 @@ fn color_ops(rgb: vec3, local_sat: f32, local_hue: f32) -> vec3 { let lab0 = oklab(rgb); var l = lab0.x; var c = sqrt(lab0.y * lab0.y + lab0.z * lab0.z); - var h = atan2(lab0.z, lab0.y); + // a neutral (black or grey) pixel has no hue: atan2(0, 0) is undefined in WGSL (NaN on some + // GPUs, which a later lightening such as the vignette then shows); the CPU's atan2 gives 0 + var h = select(atan2(lab0.z, lab0.y), 0.0, c == 0.0); if (pu(F_MIXER) != 0u) { let w = band_weights(h); var dh = 0.0; diff --git a/crates/gpu/tests/it/equivalence.rs b/crates/gpu/tests/it/equivalence.rs index 6a8169b30..d3c369454 100644 --- a/crates/gpu/tests/it/equivalence.rs +++ b/crates/gpu/tests/it/equivalence.rs @@ -494,6 +494,34 @@ fn clipped_highlights_stay_neutral_on_both() { } } +/// Black pixels have no hue. The GPU's `atan2(0, 0)` is undefined (NaN on some GPUs), which made +/// every exactly black pixel NaN once a colour tool ran, and a lightening vignette then showed it: +/// the CPU lifted the black corners, the GPU left them black. +#[test] +fn black_pixels_match_under_colour_tools_and_a_light_vignette() { + if !gpu() { + return; + } + let base = scene(0, 320, 240); + // black bars across the frame, reaching into the corners the vignette lightens + let src = Arc::new(Rgb32f::from_fn(320, 240, |x, y| if y % 40 < 8 || x % 64 < 6 { [0.0; 3] } else { base.get(x, y) })); + let info = SourceInfo { raw: true, ..Default::default() }; + let req = RenderRequest::fit(320, 240); + for (name, f) in [ + ("black, light vignette + saturation", (|s: &mut DevelopSettings| s.color.saturation = 40.0) as fn(&mut DevelopSettings)), + ("black, light vignette + vibrance", |s| s.color.vibrance = 30.0), + ("black, light vignette + mixer", |s| s.mixer.yellow.hue = 30.0), + ] { + let mut s = DevelopSettings::default(); + s.vignette.amount = 25.0; + s.vignette.style = VignetteStyle::HighlightPriority; + f(&mut s); + check(name, &src, &info, &s, &req); + let gpu = lightcraft_gpu::render(&src, &info, &s, &req, None).expect("gpu render").image; + assert!(gpu.get(0, 0)[1] > 0, "{name}: the black corner is not lightened on the GPU"); + } +} + #[test] fn rendered_sources_and_other_scenes() { if !gpu() {