mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
GPU: black pixels take the colour tools and a light vignette as on the CPU
The GPU colour tools took the hue of every pixel with atan2(a, b). For a black pixel both are 0, and WGSL leaves atan2(0, 0) undefined: on an RTX 3090 (Vulkan) it is NaN, so with any colour tool on (saturation, vibrance, the mixer) every exactly black pixel became NaN. NaN is written as 0, so it stayed black and nothing showed, until a later step lightened it: with a positive vignette the CPU lifts the black corners and the GPU left them black (up to 127 levels apart in 8-bit). A neutral pixel now gets hue 0, as Rust's atan2 gives on the CPU. Regression test: black bars under a light vignette with saturation, vibrance or the mixer (fails on main: mean 12.8, max 127 levels). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e2d18b5fb9
commit
1bfa617a5a
@@ -106,7 +106,9 @@ fn color_ops(rgb: vec3<f32>, local_sat: f32, local_hue: f32) -> vec3<f32> {
|
||||
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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user