mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-06 02:20:50 +00:00
Posterize: round_away decided half-way by subtracting, and drivers may fuse the product that feeds it into one fma (AMD does), skipping the product's rounding. A straight value of 5/6 at level 4 gives 5/6 * 3 = 2.5 exactly in f32 on the CPU but 2.49999994 fused, so the GPU rounded a level down. round_away now compares x with t +/- 0.5 (exact for integer t), which no driver can fuse, and the level divide uses an fma-corrected division (div_rn) like the CPU's. CC Grid Wipe: the cell lookup floor(q / cs) used WGSL's division, which may be off by 2.5 ULP; at q = 7, cs = 3.5 AMD and Intel return just under 2 and pick the neighbouring cell. It uses div_rn too. New test: posterize matches the CPU exactly on every 8 bpc premultiplied colour and alpha at three levels (59715 of 65280 pairs differed before at level 4). RX 6900 XT, Vulkan: effectcraft-gpu 222 passed; the one failure left is CC Hex Tile, fixed by #1776. DX12 + FXC (on #1571): both tests pass. Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
2 files changed
+37
-4
No files matched your search
@@ -242,13 +242,16 @@ fn powz(x: f32, y: f32) -> f32 {
|
||||
return pow(x, y);
|
||||
}
|
||||
|
||||
// Rust's f32::round (half away from zero; WGSL's round() is half to even).
|
||||
// Rust's f32::round (half away from zero; WGSL's round() is half to even). Decided by comparing
|
||||
// `x` with `t ± 0.5` (exact for integer `t`) rather than subtracting: drivers may fuse a product
|
||||
// that feeds `x - t` into one fma (AMD does), skipping the product's rounding, so `5/6 · 3`,
|
||||
// which rounds to exactly 2.5 on the CPU, came out 0.49999994 above 2 and rounded down (#1605).
|
||||
fn round_away(x: f32) -> f32 {
|
||||
let t = trunc(x);
|
||||
if (abs(x - t) >= 0.5) {
|
||||
return t + sign(x);
|
||||
if (x >= 0.0) {
|
||||
return select(t, t + 1.0, x >= t + 0.5);
|
||||
}
|
||||
return t;
|
||||
return select(t, t - 1.0, x <= t - 0.5);
|
||||
}
|
||||
|
||||
// f32::rem_euclid(1.0).
|
||||
|
||||
@@ -288,6 +288,36 @@ fn transform_motion_blur() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #1605: Posterize matches the CPU exactly on every 8 bpc premultiplied colour and alpha. A
|
||||
/// straight value of 5/6 at level 4 rounds to exactly 2.5 on the CPU; drivers that fused the
|
||||
/// product into `round_away`'s subtraction (AMD) rounded it down a level.
|
||||
#[test]
|
||||
fn posterize_matches_the_cpu_on_every_8bpc_pair() {
|
||||
let Some(g) = crate::tests::gpu() else { return };
|
||||
let spec = effectcraft_effects::find("ec.stylize.posterize").unwrap();
|
||||
let size = [256.0, 255.0];
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||||
for level in [4.0, 7.0, 9.0] {
|
||||
params.values.insert("level".into(), n(level));
|
||||
// Pixel (x, a - 1): premultiplied colour min(x, a)/255 on alpha a/255.
|
||||
let mut img = effectcraft_raster::Image::new(256, 255);
|
||||
for a in 1..=255u32 {
|
||||
for x in 0..256u32 {
|
||||
let v = x.min(a) as f32 / 255.0;
|
||||
img.data[((a - 1) * 256 + x) as usize] = [v, v, v, a as f32 / 255.0];
|
||||
}
|
||||
}
|
||||
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale: 1.0 };
|
||||
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.0, layer_size: size, seed: 1, adjustment: false, env: Default::default() };
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
let bad: Vec<(usize, usize)> =
|
||||
cpu.img.data.iter().zip(&out.img.data).enumerate().filter(|(_, (c, o))| c != o).map(|(i, _)| ((i % 256).min(i / 256 + 1), i / 256 + 1)).collect();
|
||||
assert!(bad.is_empty(), "level {level}: {} (x, a) pairs differ from the CPU, first {:?}", bad.len(), &bad[..bad.len().min(8)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Motion Tile's Output size on an adjustment layer with a Transform after it that scales the
|
||||
/// tiles into view, and cropping (Output under 100 %): the stack's grown or cropped frame goes
|
||||
/// back onto the canvas as on the CPU (#1727).
|
||||
|
||||
Reference in new issue
Block a user