makepad/libs/box3d/tests/test_simd.rs
Admin 60ec705ab2 box3d: SIMD (SSE2/NEON), double-precision large world, snapshots, benchmarks
- contact solver wide ops + V32 now have real SSE2 and NEON paths selected
  by target arch; scalar fallback behind the disable-simd feature. All three
  paths are bit-identical (cross-arch determinism verified: same ragdoll
  hash on NEON, SSE2 under Rosetta, and scalar).
- double-precision feature (C BOX3D_DOUBLE_PRECISION): f64 world positions
  with the exact C boundary-function semantics; enables the far-from-origin
  test halves (157 tests in DP mode, 151 default).
- world snapshots: recording substrate subset (buffer/writers/geometry
  registry/readers) + world_snapshot.c port; bit-identical continuation
  after restore, corrupt-image rejection.
- examples/benchmark.rs: all 10 C benchmark scenarios; serial Rust runs
  1.05-1.55x slower than C -O2 at one worker (geomean ~1.3x with fat LTO).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:06:18 +02:00

233 lines
8.3 KiB
Rust

// Not a C test port: verifies the wide (4-lane) contact solver ops and the V32
// geometry ops produce results identical to the scalar reference on the active
// path (NEON on aarch64, SSE2 on x86_64, trivial self-check on scalar builds).
//
// The C paths are engineered to be bit-compatible: b3MulAddW is deliberately
// non-fused on every path ("Cannot use real FMA because it doesn't match the
// non-SIMD path"), and add/sub/mul/div/sqrt/min/max are IEEE operations.
// Comparison ops differ in mask REPRESENTATION (all-bits vs 1.0f) by design,
// so they are checked semantically through blend_w/all_zero_w.
use makepad_box3d::contact_solver::{
add_w, all_zero_w, blend_w, div_w, equals_w, greater_than_w, max_w, mul_add_w, mul_w, neg_w, or_w, set_w, splat_w,
sqrt_w, sub_w, sym_clamp_w, zero_w, FloatW,
};
use makepad_box3d::test_utils::{random_float, set_random_seed, RAND_SEED};
fn lanes(a: FloatW) -> [f32; 4] {
[a.get(0), a.get(1), a.get(2), a.get(3)]
}
fn assert_lanes_bits(actual: FloatW, expected: [f32; 4], what: &str) {
let a = lanes(actual);
for i in 0..4 {
assert!(
a[i].to_bits() == expected[i].to_bits(),
"{}: lane {} differs: {} ({:#010x}) vs {} ({:#010x})",
what,
i,
a[i],
a[i].to_bits(),
expected[i],
expected[i].to_bits()
);
}
}
// Scalar reference implementations (the C B3_SIMD_NONE math, per lane).
fn ref_sym_clamp(a: f32, b: f32) -> f32 {
let mut r = if a <= b { a } else { b };
r = if r <= -b { -b } else { r };
r
}
#[test]
fn wide_ops_match_scalar_reference() {
set_random_seed(RAND_SEED);
for iter in 0..1000 {
// Random lane values in [-4, 4], plus fixed edge values on the first
// iterations. Bounds for sym_clamp are non-negative like the solver's.
let mut av = [0.0f32; 4];
let mut bv = [0.0f32; 4];
for i in 0..4 {
av[i] = 4.0 * random_float();
bv[i] = 4.0 * random_float();
}
if iter == 0 {
av = [0.0, 1.0, -1.0, 3.5];
bv = [0.0, 0.0, 2.0, 3.5];
}
let a = set_w(av[0], av[1], av[2], av[3]);
let b = set_w(bv[0], bv[1], bv[2], bv[3]);
// set_w/get round trip
assert_lanes_bits(a, av, "set_w/get");
// Arithmetic: exact bit equality with the scalar computation
assert_lanes_bits(
add_w(a, b),
[av[0] + bv[0], av[1] + bv[1], av[2] + bv[2], av[3] + bv[3]],
"add_w",
);
assert_lanes_bits(
sub_w(a, b),
[av[0] - bv[0], av[1] - bv[1], av[2] - bv[2], av[3] - bv[3]],
"sub_w",
);
assert_lanes_bits(
mul_w(a, b),
[av[0] * bv[0], av[1] * bv[1], av[2] * bv[2], av[3] * bv[3]],
"mul_w",
);
if bv.iter().all(|x| *x != 0.0) {
assert_lanes_bits(
div_w(a, b),
[av[0] / bv[0], av[1] / bv[1], av[2] / bv[2], av[3] / bv[3]],
"div_w",
);
}
assert_lanes_bits(neg_w(a), [-av[0], -av[1], -av[2], -av[3]], "neg_w");
let abs_a = set_w(av[0].abs(), av[1].abs(), av[2].abs(), av[3].abs());
assert_lanes_bits(
sqrt_w(abs_a),
[av[0].abs().sqrt(), av[1].abs().sqrt(), av[2].abs().sqrt(), av[3].abs().sqrt()],
"sqrt_w",
);
// Non-fused a + b * c on every path
let c = splat_w(1.5);
assert_lanes_bits(
mul_add_w(a, b, c),
[av[0] + bv[0] * 1.5, av[1] + bv[1] * 1.5, av[2] + bv[2] * 1.5, av[3] + bv[3] * 1.5],
"mul_add_w",
);
assert_lanes_bits(
max_w(a, b),
[
if av[0] >= bv[0] { av[0] } else { bv[0] },
if av[1] >= bv[1] { av[1] } else { bv[1] },
if av[2] >= bv[2] { av[2] } else { bv[2] },
if av[3] >= bv[3] { av[3] } else { bv[3] },
],
"max_w",
);
// sym_clamp with non-negative bounds (solver usage).
// VALUE equality, not bit equality: at an exact a == b == 0 tie the C
// scalar branch chain produces -0 while the C SIMD min/max path
// produces +0 (same divergence exists between the C paths). The values
// are equal so downstream arithmetic is unaffected.
let bounds = set_w(bv[0].abs(), bv[1].abs(), bv[2].abs(), bv[3].abs());
let sc = lanes(sym_clamp_w(a, bounds));
for i in 0..4 {
let expected = ref_sym_clamp(av[i], bv[i].abs());
assert!(
sc[i] == expected,
"sym_clamp_w lane {}: {} vs {}",
i,
sc[i],
expected
);
}
// Comparisons: mask representation differs per path (all-bits vs 1.0),
// so check them semantically through blend_w.
let gt = greater_than_w(a, b);
let sel = lanes(blend_w(a, b, gt));
for i in 0..4 {
let expected = if av[i] > bv[i] { bv[i] } else { av[i] };
assert!(sel[i].to_bits() == expected.to_bits(), "greater_than/blend lane {}", i);
}
let eq = equals_w(a, a);
let sel = lanes(blend_w(a, b, eq));
for i in 0..4 {
assert!(sel[i].to_bits() == bv[i].to_bits(), "equals/blend lane {}", i);
}
// or of two masks selects where either condition holds
let m = or_w(greater_than_w(a, b), equals_w(a, b));
let sel = lanes(blend_w(a, b, m));
for i in 0..4 {
let expected = if av[i] >= bv[i] { bv[i] } else { av[i] };
assert!(sel[i].to_bits() == expected.to_bits(), "or/blend lane {}", i);
}
// all_zero
assert!(all_zero_w(zero_w()));
assert!(all_zero_w(splat_w(0.0)));
if av.iter().any(|x| *x != 0.0) {
assert!(!all_zero_w(a));
}
// lane set
let mut d = a;
d.set(2, 42.5);
assert!(d.get(0) == av[0] && d.get(1) == av[1] && d.get(2) == 42.5 && d.get(3) == av[3]);
}
}
#[test]
fn v32_ops_match_scalar_reference() {
use makepad_box3d::simd::*;
set_random_seed(RAND_SEED + 1);
for _ in 0..1000 {
let ax = 4.0 * random_float();
let ay = 4.0 * random_float();
let az = 4.0 * random_float();
let bx = 4.0 * random_float();
let by = 4.0 * random_float();
let bz = 4.0 * random_float();
let a = load_v(&[ax, ay, az]);
let b = load_v(&[bx, by, bz]);
// Lane accessors
assert!(get_x_v(a) == ax && get_y_v(a) == ay && get_z_v(a) == az);
assert!(get_v(a, 0) == ax && get_v(a, 1) == ay && get_v(a, 2) == az);
// Arithmetic: bit equality per lane
let s = add_v(a, b);
assert!(get_x_v(s).to_bits() == (ax + bx).to_bits());
assert!(get_y_v(s).to_bits() == (ay + by).to_bits());
assert!(get_z_v(s).to_bits() == (az + bz).to_bits());
let d = sub_v(a, b);
assert!(get_x_v(d).to_bits() == (ax - bx).to_bits());
let m = mul_v(a, b);
assert!(get_y_v(m).to_bits() == (ay * by).to_bits());
let n = neg_v(a);
assert!(get_z_v(n).to_bits() == (0.0f32 - az).to_bits());
// cross product per the scalar formula
let cr = cross_v(a, b);
assert!(get_x_v(cr).to_bits() == (ay * bz - az * by).to_bits());
assert!(get_y_v(cr).to_bits() == (az * bx - ax * bz).to_bits());
assert!(get_z_v(cr).to_bits() == (ax * by - ay * bx).to_bits());
let mc = modified_cross_v(a, b);
assert!(get_x_v(mc).to_bits() == (ay * bz + az * by).to_bits());
// Comparisons (low 3 lanes)
assert!(any_less_3v(a, b) == (ax < bx || ay < by || az < bz));
assert!(any_greater_3v(a, b) == (ax > bx || ay > by || az > bz));
assert!(all_less_eq_3v(a, b) == (ax <= bx && ay <= by && az <= bz));
assert!(any_less_eq_3v(a, b) == (ax <= bx || ay <= by || az <= bz));
// abs/min/max
let ab = abs_v(a);
assert!(get_x_v(ab) == ax.abs() && get_y_v(ab) == ay.abs() && get_z_v(ab) == az.abs());
let mn = min_v(a, b);
assert!(get_x_v(mn) == if ax < bx { ax } else { bx });
let mx = max_v(a, b);
assert!(get_x_v(mx) == if ax > bx { ax } else { bx });
}
}