Merge pull request #3394 from MatejKozlovsky/gpu-box-blur-match

GPU: Fast Box Blur and Gaussian Blur rows round like the CPU
This commit is contained in:
ash
2026-11-11 09:51:54 +05:30
committed by GitHub
3 changed files with 33 additions and 2 deletions
+3 -1
View File
@@ -274,7 +274,9 @@ pub(crate) fn box_passes(e: &mut Enc, img: &GpuImage, rx: &[usize], ry: &[usize]
let mut cur = img.clone();
let passes = rx.iter().map(|&r| (false, r)).chain(ry.iter().map(|&r| (true, r))).filter(|p| p.1 > 0);
for (vertical, r) in passes {
let block = if vertical { (4 * r).clamp(32, 256) } else { 256 } as u32;
// Vertical sums restart every `block` rows as the CPU's do; a row is one running sum, as
// on the CPU (restarting it rounds differently).
let block = if vertical { (4 * r).clamp(32, 256) as u32 } else { cur.width.max(1) };
let (lines, n) = if vertical { (cur.width, cur.height) } else { (cur.height, cur.width) };
let mut p = Params::default();
p.u[0] = [r as u32, repeat as u32, block, 0];
+1 -1
View File
@@ -270,7 +270,7 @@ fn line_get(horizontal: bool, line: i32, i: i32, n: i32, repeat: bool) -> vec4<f
}
// One box pass of radius r along rows (horizontal) or columns, as running sums over blocks of
// `block` pixels (each block restarts its sum, as the CPU's vertical pass does).
// `block` pixels (each block restarts its sum, as the CPU's vertical pass does; rows are one block).
fn box_pass(gid: vec3<u32>, horizontal: bool) {
let dims = out_dims();
let r = i32(P.u[0].x);
+29
View File
@@ -731,6 +731,35 @@ fn small_blurs_fall_back_to_the_cpu() {
}
}
/// Horizontal GPU box passes used to restart the running sum every 256 px; the CPU keeps one sum
/// per row, so images wider than 256 disagreed in f32 rounding. Width 320 forces that path.
/// Exact on llvmpipe (CI lavapipe), which fails without the fix; other adapters use the usual
/// Bpc32 tolerance because a tighter bound can fail on unmeasured hardware and does not catch
/// the regression (Metal still differs by ~1e-6 with or without the fix).
#[test]
fn box_blur_horizontal_rows_match_cpu_beyond_256() {
let Some(g) = gpu() else { return };
let img = pattern(11, 320, 48);
let up = g.ctx.upload_image(&img).expect("upload");
let tol = if on_llvmpipe() { 0.0 } else { tolerance(BitDepth::Bpc32) };
let check_case = |name: &str, want: Image, got: Image| {
assert_eq!((got.width, got.height), (want.width, want.height), "{name}: size");
let d = diff(&want, &got, tol);
assert_eq!(d.over, 0, "{name}: max {} at {:?} on {}", d.max, d.worst, g.ctx.name);
};
for (name, r) in [("box r5×3", 5usize), ("box r25×3", 25)] {
let mut e = crate::context::Enc::new(&g.ctx);
let rx = [r, r, r];
let out = crate::effects::box_passes(&mut e, &up, &rx, &rx, false);
check_case(name, effectcraft_raster::box_blur(&img, r, r, 3, false), e.download(&out).expect("download"));
}
for (name, sigma) in [("gauss σ10", 10.0f64), ("gauss σ40", 40.0)] {
let mut e = crate::context::Enc::new(&g.ctx);
let out = crate::effects::gaussian_blur(&mut e, &up, sigma, sigma, false);
check_case(name, effectcraft_raster::gaussian_blur(&img, sigma, sigma, false), e.download(&out).expect("download"));
}
}
#[test]
fn gpu_effects_match_cpu() {
// Sigma 3 px and up and even radii, so the half-scale pass stays on the GPU too (#1939).