- 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>
233 lines
8.3 KiB
Rust
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 });
|
|
}
|
|
}
|