makepad/libs/box3d/tests/test_math.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

316 lines
11 KiB
Rust

// Port of box3d/test/test_math.c
use makepad_box3d::math_functions::*;
use makepad_box3d::math_internal::*;
use makepad_box3d::test_utils::random_float;
use makepad_box3d::{ensure, ensure_small};
// 0.0023 degrees
const ATAN_TOL: f32 = 0.00004;
#[test]
fn math_test() {
let mut t = -10.0f32;
while t < 10.0 {
let angle = PI * t;
let cs = compute_cos_sin(angle);
let c = angle.cos();
let s = angle.sin();
// The cosine and sine approximations are accurate to about 0.1 degrees (0.002 radians)
ensure_small!(cs.cosine - c, 0.002);
ensure_small!(cs.sine - s, 0.002);
let xn = unwind_angle(angle);
let a = atan2(s, c);
ensure!(is_valid_float(a));
let mut diff = abs_float(a - xn);
// The two results can be off by 360 degrees (-pi and pi)
if diff > PI {
diff -= 2.0 * PI;
}
// The approximate atan2 is quite accurate
ensure_small!(diff, ATAN_TOL);
t += 0.01;
}
let mut y = -1.0f32;
while y <= 1.0 {
let mut x = -1.0f32;
while x <= 1.0 {
let a1 = atan2(y, x);
let a2 = y.atan2(x);
let diff = abs_float(a1 - a2);
ensure!(is_valid_float(a1));
ensure_small!(diff, ATAN_TOL);
x += 0.01;
}
y += 0.01;
}
for (yy, xx) in [(1.0f32, 0.0f32), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0), (0.0, 0.0)] {
let a1 = atan2(yy, xx);
let a2 = yy.atan2(xx);
let diff = abs_float(a1 - a2);
ensure!(is_valid_float(a1));
ensure_small!(diff, ATAN_TOL);
}
let zero = Vec3::ZERO;
let one = vec3(1.0, 1.0, 1.0);
let two = vec3(2.0, 2.0, 2.0);
let mut v = add(one, two);
ensure!(v.x == 3.0 && v.y == 3.0);
v = sub(zero, two);
ensure!(v.x == -2.0 && v.y == -2.0);
v = add(two, two);
ensure!(v.x != 5.0 && v.y != 5.0);
let axis = normalize(vec3(-0.75, 0.5, 1.0));
let transform1 = Transform { p: vec3(-2.0, 3.0, 0.0), q: Quat::IDENTITY };
let transform2 = Transform { p: vec3(1.0, 0.0, 0.0), q: make_quat_from_axis_angle(axis, PI) };
let transform = mul_transforms(transform2, transform1);
v = transform_point(transform2, transform_point(transform1, two));
let mut u = transform_point(transform, two);
ensure_small!(u.x - v.x, 10.0 * f32::EPSILON);
ensure_small!(u.y - v.y, 10.0 * f32::EPSILON);
v = transform_point(transform1, two);
v = inv_transform_point(transform1, v);
ensure_small!(v.x - two.x, 8.0 * f32::EPSILON);
ensure_small!(v.y - two.y, 8.0 * f32::EPSILON);
let rel_transform = inv_mul_transforms(transform1, transform2);
v = inv_transform_point(transform1, transform_point(transform2, two));
u = transform_point(rel_transform, two);
ensure_small!(u.x - v.x, 10.0 * f32::EPSILON);
ensure_small!(u.y - v.y, 10.0 * f32::EPSILON);
{
let axis = vec3(0.0, 0.0, 1.0);
let q1 = make_quat_from_axis_angle(axis, -0.5 * PI);
let q2 = compute_quat_between_unit_vectors(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
ensure_small!(q1.v.x - q2.v.x, f32::EPSILON);
ensure_small!(q1.v.y - q2.v.y, f32::EPSILON);
ensure_small!(q1.v.z - q2.v.z, f32::EPSILON);
ensure_small!(q1.s - q2.s, f32::EPSILON);
let q3 = normalize_quat(Quat { v: vec3(1.0, -2.0, 3.0), s: 4.0 });
let q4 = inv_mul_quat(q3, q1);
let q5 = mul_quat(q3, q4);
ensure_small!(q1.v.x - q5.v.x, f32::EPSILON);
ensure_small!(q1.v.y - q5.v.y, f32::EPSILON);
ensure_small!(q1.v.z - q5.v.z, f32::EPSILON);
ensure_small!(q1.s - q5.s, f32::EPSILON);
let q6 = compute_quat_between_unit_vectors(vec3(0.0, 1.0, 0.0), vec3(0.0, -1.0, 0.0));
ensure_small!(q6.s, f32::EPSILON);
}
let v = normalize(vec3(0.2, -0.5, 3.0));
let mut z = -1.0f32;
while z <= 1.0 {
let mut y = -1.0f32;
while y <= 1.0 {
let mut x = -1.0f32;
while x <= 1.0 {
if x == 0.0 && y == 0.0 && z == 0.0 {
x += 0.02;
continue;
}
let u = normalize(vec3(x, y, z));
let r = compute_quat_between_unit_vectors(v, u);
ensure!(is_valid_quat(r));
let w = rotate_vector(r, v);
ensure_small!(dot(r.v, cross(u, w)) - scalar_triple_product(r.v, u, w), f32::EPSILON);
// The quaternion between vectors can have lots of round off error at large angles.
ensure_small!(w.x - u.x, 0.001);
ensure_small!(w.y - u.y, 0.001);
ensure_small!(w.z - u.z, 0.001);
// Twist angle testing
let mut twist = if r.s < 0.0 { atan2(-r.v.z, -r.s) } else { atan2(r.v.z, r.s) };
twist *= 2.0;
ensure!(-PI <= twist && twist <= PI);
x += 0.02;
}
y += 0.02;
}
z += 0.02;
}
{
// More twist angle testing
let q = Quat { v: vec3(-0.0558656752, -0.188799798, 0.00689807534), s: -0.980401039 };
let mut twist = if q.s < 0.0 { atan2(-q.v.z, -q.s) } else { atan2(q.v.z, q.s) };
twist *= 2.0;
ensure!(-PI <= twist && twist <= PI);
}
{
let m = Matrix3 {
cx: vec3(3.0, 1.0, -1.0),
cy: vec3(-1.0, 3.0, 1.0),
cz: vec3(1.0, -1.0, 3.0),
};
let inv_m = invert_matrix(m);
let a = mul_mm(m, inv_m);
ensure_small!(a.cx.x - 1.0, f32::EPSILON);
ensure_small!(a.cx.y, f32::EPSILON);
ensure_small!(a.cx.z, f32::EPSILON);
ensure_small!(a.cy.x, f32::EPSILON);
ensure_small!(a.cy.y - 1.0, f32::EPSILON);
ensure_small!(a.cy.z, f32::EPSILON);
ensure_small!(a.cz.x, f32::EPSILON);
ensure_small!(a.cz.y, f32::EPSILON);
ensure_small!(a.cz.z - 1.0, f32::EPSILON);
let v = vec3(1.0, -2.0, 3.0);
let u = mul_mv(inv_m, mul_mv(m, v));
ensure_small!(v.x - u.x, f32::EPSILON);
ensure_small!(v.y - u.y, f32::EPSILON);
ensure_small!(v.z - u.z, f32::EPSILON);
let w = mul_mv(inv_m, v);
let u = solve3(m, v);
ensure_small!(w.x - u.x, f32::EPSILON);
ensure_small!(w.y - u.y, f32::EPSILON);
ensure_small!(w.z - u.z, f32::EPSILON);
}
{
let m = Matrix2 { cx: vec2(3.0, 1.0), cy: vec2(-1.0, 3.0) };
let inv_m = invert2(m);
let a = mul_mm2(m, inv_m);
ensure_small!(a.cx.x - 1.0, f32::EPSILON);
ensure_small!(a.cx.y, f32::EPSILON);
ensure_small!(a.cy.x, f32::EPSILON);
ensure_small!(a.cy.y - 1.0, f32::EPSILON);
let v2 = vec2(1.0, -2.0);
let mut u2 = mul_mv2(inv_m, mul_mv2(m, v2));
ensure_small!(v2.x - u2.x, f32::EPSILON);
ensure_small!(v2.y - u2.y, f32::EPSILON);
let w = mul_mv2(inv_m, v2);
u2 = solve2(m, v2);
ensure_small!(w.x - u2.x, f32::EPSILON);
ensure_small!(w.y - u2.y, f32::EPSILON);
let w = mul_mv2(m, u2);
ensure_small!(w.x - v2.x, 10.0 * f32::EPSILON);
ensure_small!(w.y - v2.y, 10.0 * f32::EPSILON);
}
for _ in 0..100 {
let a = random_float();
let b = a as f64;
let c = b as f32;
ensure!(c == a);
}
let q1 = Quat::IDENTITY;
let q2 = make_quat_from_axis_angle(Vec3::AXIS_Z, 0.5 * PI);
let n = 100;
for i in 0..=n {
let alpha = i as f32 / n as f32;
let q = nlerp(q1, q2, alpha);
let angle = get_twist_angle(q);
ensure_small!(alpha * 0.5 * PI - angle, 1.0 * DEG_TO_RAD);
}
{
let normal = vec3(0.504055440, 0.621548057, 0.599671543);
let perp = arbitrary_perp(normal);
ensure_small!(dot(normal, perp), 2.0 * f32::EPSILON);
}
{
// World position boundary helpers. The query agrees with the built type sizes.
ensure!(
makepad_box3d::core::is_double_precision()
== (std::mem::size_of::<Pos>() > std::mem::size_of::<Vec3>())
);
// Deltas and offsets round trip exactly for representable inputs in both modes.
let a = vec3(3.0, -5.0, 2.0);
let b = vec3(1.0, 4.0, -6.0);
let pa = to_pos(a);
let pb = to_pos(b);
let d = sub_pos(pa, pb);
let s = sub(a, b);
ensure!(d.x == s.x && d.y == s.y && d.z == s.z);
let back = sub_pos(offset_pos(pb, s), pa);
ensure!(back.x == 0.0 && back.y == 0.0 && back.z == 0.0);
let r = to_vec3(pa);
ensure!(r.x == a.x && r.y == a.y && r.z == a.z);
ensure!(is_valid_position(pa));
// World transform relative ops match the pure float transform ops.
let axis = normalize(vec3(0.3, -0.7, 0.5));
let t_a = Transform { p: a, q: make_quat_from_axis_angle(axis, 0.4) };
let t_b = Transform { p: b, q: make_quat_from_axis_angle(axis, -1.1) };
let w_a = make_world_transform(t_a);
let w_b = make_world_transform(t_b);
ensure!(is_valid_world_transform(w_a));
let rel_ref = inv_mul_transforms(t_a, t_b);
let rel = inv_mul_world_transforms(w_a, w_b);
ensure_small!(rel.p.x - rel_ref.p.x, 1.0e-5);
ensure_small!(rel.p.y - rel_ref.p.y, 1.0e-5);
ensure_small!(rel.p.z - rel_ref.p.z, 1.0e-5);
ensure_small!(rel.q.s - rel_ref.q.s, 1.0e-5);
// Local point to world and back.
let local = vec3(0.5, -0.25, 1.5);
let back2 = inv_transform_world_point(w_a, transform_world_point(w_a, local));
ensure_small!(back2.x - local.x, 1.0e-5);
ensure_small!(back2.y - local.y, 1.0e-5);
ensure_small!(back2.z - local.z, 1.0e-5);
// Compose with a local transform, then strip it back off.
let rel_ab = inv_mul_world_transforms(w_a, mul_world_transforms(w_a, t_b));
ensure_small!(rel_ab.p.x - t_b.p.x, 1.0e-5);
ensure_small!(rel_ab.p.y - t_b.p.y, 1.0e-5);
ensure_small!(rel_ab.p.z - t_b.p.z, 1.0e-5);
}
}
// Port of the BOX3D_DOUBLE_PRECISION block in test_math.c: far from the origin the
// double layer keeps the relative result accurate where pure float would quantize.
// Two poses one meter apart at x = 1e8.
#[cfg(feature = "double-precision")]
#[test]
fn math_double_precision_relative_transform() {
use makepad_box3d::math_functions::{inv_mul_world_transforms, offset_pos, Pos, WorldTransform};
use makepad_box3d::ensure;
let base = Pos { x: 1.0e8, y: 0.0, z: 0.0 };
let w_a = WorldTransform { p: base, q: Quat::IDENTITY };
let w_b = WorldTransform { p: offset_pos(base, vec3(1.0, 0.0, 0.0)), q: Quat::IDENTITY };
let rel = inv_mul_world_transforms(w_a, w_b);
ensure!(rel.p.x == 1.0 && rel.p.y == 0.0 && rel.p.z == 0.0);
}