mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-15 19:56:59 -04:00
GPU: decide warped-geometry edges with a CPU coverage mask
The lens + perspective warp classified source-edge pixels in f32 on the GPU. A position 0.000035 px outside the edge rounded inside, so the GPU sampled photo pixels where the CPU reference draws blank canvas (geometry_variants, "embedded lens + perspective", max 91 LSB). The CPU now evaluates the f64 framing decision into a one-bit-per-pixel mask; sampling and colour stay on the GPU. The embedded-lens cases move to their own test, embedded_lens_perspective_edges_match, with the same tolerances. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e99ce5a578
commit
94fe30015c
5 files changed
+47
-8
No files matched your search
@@ -62,7 +62,7 @@ const MODULES: &[Module] = &[
|
||||
},
|
||||
Module {
|
||||
src: include_str!("wgsl/geom.wgsl"),
|
||||
bindings: &[("src", false, "f32"), ("dst", true, "f32")],
|
||||
bindings: &[("src", false, "f32"), ("dst", true, "f32"), ("coverage", false, "u32")],
|
||||
entries: &["orient", "sample_affine", "sample_warp"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -422,7 +422,7 @@ fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, src_buf: Arc<Buf>, plan: &Plan<'_>
|
||||
let out = cx.gpu.buffer(sp.ow * sp.oh * 3);
|
||||
let mut p = vec![src.width as u32, src.height as u32, sp.ow as u32, sp.oh as u32];
|
||||
p.extend(orient_map(fr.orient, src.width, src.height).map(|v| v as u32));
|
||||
cx.run("orient", &p, &[Some(&src_buf), Some(&out)], groups2(sp.ow, sp.oh, [16, 16]));
|
||||
cx.run("orient", &p, &[Some(&src_buf), Some(&out), None], groups2(sp.ow, sp.oh, [16, 16]));
|
||||
Arc::new(out)
|
||||
};
|
||||
let (base, (bw, bh)) = match sp.prefilter {
|
||||
@@ -433,7 +433,7 @@ fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, src_buf: Arc<Buf>, plan: &Plan<'_>
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let mut p = vec![bw as u32, bh as u32, w as u32, h as u32];
|
||||
p.extend(affine_bits(xf));
|
||||
cx.run("sample_affine", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
cx.run("sample_affine", &p, &[Some(&base), Some(&out), None], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
};
|
||||
match (&sp.mode, fr.warp.as_ref()) {
|
||||
@@ -444,7 +444,26 @@ fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, src_buf: Arc<Buf>, plan: &Plan<'_>
|
||||
(SampleMode::Warp(o2t), Some(wp)) => {
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let p = warp_params(wp, (bw, bh), (w, h), o2t, sp.sx, sp.sy);
|
||||
cx.run("sample_warp", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
// f32 rounding can cross a source edge. Keep the reference f64
|
||||
// framing decision in a bit mask; color sampling stays on the GPU.
|
||||
let words = w.div_ceil(32);
|
||||
let mut coverage = vec![0u32; words * h];
|
||||
lightcraft_raster::par_rows(&mut coverage, words, |y, row| {
|
||||
for (word, bits) in row.iter_mut().enumerate() {
|
||||
for bit in 0..32 {
|
||||
let x = word * 32 + bit;
|
||||
if x >= w {
|
||||
break;
|
||||
}
|
||||
let t = o2t.apply(lightcraft_geom::Point::new(x as f64 + 0.5, y as f64 + 0.5));
|
||||
if wp.inside(wp.corrected_to_source(wp.to_corrected(t), 1)) {
|
||||
*bits |= 1 << bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let coverage = cx.gpu.upload(&coverage);
|
||||
cx.run("sample_warp", &p, &[Some(&base), Some(&out), Some(&coverage)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,9 @@ fn sample_warp(@builtin(global_invocation_id) g: vec3<u32>) {
|
||||
let s = corrected_to_source(c, 1u);
|
||||
let sx = pf(10u);
|
||||
let sy = pf(11u);
|
||||
if (!(s.x >= -0.5 && s.y >= -0.5 && s.x <= pf(12u) + 0.5 && s.y <= pf(13u) + 0.5)) {
|
||||
// CPU reference coverage avoids an f32 rounding discontinuity at the edge.
|
||||
let word = g.y * ((w + 31u) / 32u) + g.x / 32u;
|
||||
if ((coverage[word] & (1u << (g.x % 32u))) == 0u) {
|
||||
put(g.y * w + g.x, vec3<f32>(BLANK_R, BLANK_G, BLANK_B));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -430,7 +430,6 @@ fn geometry_variants() {
|
||||
if !gpu() {
|
||||
return;
|
||||
}
|
||||
use lightcraft_develop::{EmbeddedLens, EmbeddedVignette, EmbeddedWarp};
|
||||
let src = scene(2, 900, 600);
|
||||
let raw = SourceInfo { raw: true, ..Default::default() };
|
||||
use Orientation::*;
|
||||
@@ -445,7 +444,19 @@ fn geometry_variants() {
|
||||
check(&format!("{o:?} crop={crop}"), &src, &raw, &s, &req);
|
||||
}
|
||||
}
|
||||
// embedded DNG lens corrections (per-plane warp + vignette) with manual CA
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_lens_perspective_edges_match() {
|
||||
if !gpu() {
|
||||
return;
|
||||
}
|
||||
use lightcraft_develop::{EmbeddedLens, EmbeddedVignette, EmbeddedWarp};
|
||||
let src = scene(2, 900, 600);
|
||||
let raw = SourceInfo { raw: true, ..Default::default() };
|
||||
// The unrotated case includes a position only 0.000035 px outside the edge.
|
||||
// f32 classification previously sampled the photo instead of blank canvas.
|
||||
// Embedded DNG lens corrections (per-plane warp + vignette) with manual CA.
|
||||
let lens = EmbeddedLens {
|
||||
warp: Some(EmbeddedWarp {
|
||||
planes: [[1.0, -0.03, 0.01, 0.0, 0.001, -0.002], [1.0, -0.028, 0.01, 0.0, 0.001, -0.002], [1.0, -0.026, 0.01, 0.0, 0.001, -0.002]],
|
||||
@@ -460,7 +471,7 @@ fn geometry_variants() {
|
||||
s.optics.ca_red = 30.0;
|
||||
s.geometry.horizontal = -15.0;
|
||||
check("embedded lens + perspective", &src, &info, &s, &RenderRequest::fit(700, 700));
|
||||
s.orientation = Rotate270;
|
||||
s.orientation = Orientation::Rotate270;
|
||||
check("embedded lens rotated", &src, &info, &s, &RenderRequest::fit(700, 700));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@ The CPU pipeline (`crates/pipeline`) is the reference ("oracle"). `lightcraft-gp
|
||||
evaluates the same stages with wgpu compute shaders (WGSL) on Metal / Vulkan / DX12. Everything is
|
||||
pure Rust (wgpu, naga); the drivers are the system's. No GL backend is compiled in.
|
||||
|
||||
Warped geometry uses a CPU-computed coverage bit mask for the source boundary.
|
||||
The CPU reference evaluates that boundary in double precision; GPU float
|
||||
rounding otherwise can turn a blank edge pixel into a photo pixel. The mask
|
||||
uses one bit per output pixel (rows padded to 32 bits). Sampling and color
|
||||
corrections remain on the GPU, and existing geometry stage caching reuses the
|
||||
result. This adds CPU coordinate evaluation only when warped geometry rebuilds.
|
||||
|
||||
## Backends, environment variables and troubleshooting (issue #136)
|
||||
wgpu loads the driver of **every** backend in an instance's set while it enumerates adapters — even
|
||||
when it then picks another one. A Vulkan driver that crashes there (issue #136: an access violation
|
||||
|
||||
Reference in new issue
Block a user