- 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>
287 lines
10 KiB
Rust
287 lines
10 KiB
Rust
// Port of box3d/test/test_large_world.c
|
|
// The Rust port is single precision only (BOX3D_DOUBLE_PRECISION off), so the
|
|
// far-from-origin halves of each C subtest (gated behind the define) are not
|
|
// ported; the origin halves are kept in full.
|
|
|
|
use makepad_box3d::body::*;
|
|
use makepad_box3d::hull::{make_box_hull, make_cube_hull};
|
|
use makepad_box3d::math_functions::{offset_pos, pos, sub_pos, vec3, Pos, Vec3};
|
|
use makepad_box3d::physics_world::*;
|
|
use makepad_box3d::shape::{create_hull_shape, create_sphere_shape, shape_ray_cast};
|
|
use makepad_box3d::types::*;
|
|
use makepad_box3d::{ensure, ensure_small};
|
|
|
|
const STACK_COUNT: usize = 6;
|
|
const MAX_STEPS: i32 = 400;
|
|
|
|
struct StackResult {
|
|
// Final body positions relative to the base, so origin and far runs are directly comparable
|
|
relative_positions: [Vec3; STACK_COUNT],
|
|
|
|
// First step on which the top body fell asleep, or -1 if it never settled
|
|
sleep_step: i32,
|
|
}
|
|
|
|
// Drop a short stack of boxes onto a ground box centered at baseX. Records each body's final
|
|
// position relative to the base and the step on which the stack settles.
|
|
fn run_stack(base_x: f32) -> StackResult {
|
|
let base: Pos = pos(base_x, 0.0, 0.0);
|
|
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut ground_def = default_body_def();
|
|
ground_def.position = base;
|
|
let ground_id = create_body(&mut world, &ground_def);
|
|
let ground_box = make_box_hull(10.0, 1.0, 10.0);
|
|
let ground_shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
|
|
|
|
let mut bodies = [makepad_box3d::id::NULL_BODY_ID; STACK_COUNT];
|
|
for i in 0..STACK_COUNT {
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.position = offset_pos(base, vec3(0.0, 2.0 + 1.05 * i as f32, 0.0));
|
|
bodies[i] = create_body(&mut world, &body_def);
|
|
|
|
let cube = make_cube_hull(0.5);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.density = 1.0;
|
|
create_hull_shape(&mut world, bodies[i], &shape_def, &cube);
|
|
}
|
|
|
|
let mut result = StackResult { relative_positions: [Vec3::ZERO; STACK_COUNT], sleep_step: -1 };
|
|
|
|
for step in 0..MAX_STEPS {
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
if result.sleep_step < 0 && !body_is_awake(&world, bodies[STACK_COUNT - 1]) {
|
|
result.sleep_step = step;
|
|
}
|
|
}
|
|
|
|
for i in 0..STACK_COUNT {
|
|
let p = body_get_position(&world, bodies[i]);
|
|
result.relative_positions[i] = sub_pos(p, base);
|
|
}
|
|
|
|
destroy_world(world);
|
|
result
|
|
}
|
|
|
|
// A stack at the origin should settle. (The C far-from-origin comparison is
|
|
// double precision only and is not ported.)
|
|
#[test]
|
|
fn large_world_stack_test() {
|
|
let origin = run_stack(0.0);
|
|
ensure!(origin.sleep_step >= 0);
|
|
}
|
|
|
|
// Fire a fast bullet at a thin wall. Returns the bullet's final x relative to the base. If
|
|
// continuous collision works the bullet stops at the wall instead of tunneling past it.
|
|
fn run_bullet(base_x: f32) -> f32 {
|
|
let base: Pos = pos(base_x, 0.0, 0.0);
|
|
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
// Thin static wall at x = base + 5, spanning y and z
|
|
let mut wall_def = default_body_def();
|
|
wall_def.body_type = BodyType::Static;
|
|
wall_def.position = offset_pos(base, vec3(5.0, 0.0, 0.0));
|
|
let wall_id = create_body(&mut world, &wall_def);
|
|
let wall_box = make_box_hull(0.05, 5.0, 5.0);
|
|
let wall_shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, wall_id, &wall_shape_def, &wall_box);
|
|
|
|
// Small fast bullet aimed at the wall, no gravity
|
|
let mut bullet_def = default_body_def();
|
|
bullet_def.body_type = BodyType::Dynamic;
|
|
bullet_def.is_bullet = true;
|
|
bullet_def.gravity_scale = 0.0;
|
|
bullet_def.position = base;
|
|
bullet_def.linear_velocity = vec3(200.0, 0.0, 0.0);
|
|
let bullet_id = create_body(&mut world, &bullet_def);
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.1 };
|
|
let mut bullet_shape_def = default_shape_def();
|
|
bullet_shape_def.density = 1.0;
|
|
create_sphere_shape(&mut world, bullet_id, &bullet_shape_def, &sphere);
|
|
|
|
for _step in 0..30 {
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
}
|
|
|
|
let relative = sub_pos(body_get_position(&world, bullet_id), base);
|
|
destroy_world(world);
|
|
relative.x
|
|
}
|
|
|
|
// The bullet must be caught by the wall, not tunnel through it.
|
|
#[test]
|
|
fn large_world_bullet_test() {
|
|
// Wall front face is at x = 5 - 0.05; the bullet radius is 0.1, so a caught bullet stays well
|
|
// short of the wall center at x = 5.
|
|
let origin_x = run_bullet(0.0);
|
|
ensure!(origin_x < 5.0);
|
|
}
|
|
|
|
struct QueryResult {
|
|
cast_hit: bool,
|
|
cast_rel_x: f32, // shape cast hit point x relative to the base
|
|
overlap_hit: bool,
|
|
mover_fraction: f32,
|
|
plane_count: i32,
|
|
ray_hit: bool,
|
|
ray_rel_x: f32, // world ray cast hit point x relative to the base
|
|
shape_ray_hit: bool,
|
|
shape_ray_rel_x: f32, // direct shape ray cast hit point x relative to the base
|
|
}
|
|
|
|
// Run the four origin relative spatial queries against a static box centered at the base. The query
|
|
// inputs are all relative to the base, so passing base as the origin keeps them precise far out.
|
|
fn run_queries(base_x: f32) -> QueryResult {
|
|
let base: Pos = pos(base_x, 0.0, 0.0);
|
|
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Static;
|
|
body_def.position = base;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
let box_hull = make_box_hull(1.0, 1.0, 1.0);
|
|
let shape_def = default_shape_def();
|
|
let shape_id = create_hull_shape(&mut world, body_id, &shape_def, &box_hull);
|
|
|
|
world_step(&mut world, 1.0 / 60.0, 1);
|
|
|
|
// Sphere proxy swept from the left into the box, hitting the left face at relative x = -1
|
|
let cast_point = [vec3(-5.0, 0.0, 0.0)];
|
|
let cast_proxy = ShapeProxy { points: &cast_point, radius: 0.25 };
|
|
let mut cast_hit = false;
|
|
let mut cast_rel_x = 0.0f32;
|
|
world_cast_shape(
|
|
&world,
|
|
base,
|
|
&cast_proxy,
|
|
vec3(10.0, 0.0, 0.0),
|
|
default_query_filter(),
|
|
&mut |_shape_id, point, _normal, fraction, _material_id, _triangle_index, _child_index| {
|
|
cast_hit = true;
|
|
cast_rel_x = sub_pos(point, base).x;
|
|
fraction
|
|
},
|
|
);
|
|
|
|
// Sphere proxy sitting at the box center
|
|
let overlap_point = [vec3(0.0, 0.0, 0.0)];
|
|
let overlap_proxy = ShapeProxy { points: &overlap_point, radius: 0.5 };
|
|
let mut overlap_hit = false;
|
|
world_overlap_shape(&world, base, &overlap_proxy, default_query_filter(), &mut |_shape_id| {
|
|
overlap_hit = true;
|
|
true
|
|
});
|
|
|
|
// Capsule swept from the left into the box
|
|
let mover_cast = Capsule { center1: vec3(-5.0, -0.3, 0.0), center2: vec3(-5.0, 0.3, 0.0), radius: 0.25 };
|
|
let mover_fraction =
|
|
world_cast_mover(&world, base, &mover_cast, vec3(10.0, 0.0, 0.0), default_query_filter(), None);
|
|
|
|
// Capsule overlapping the left face, should report a contact plane
|
|
let mover_collide = Capsule { center1: vec3(-1.1, -0.3, 0.0), center2: vec3(-1.1, 0.3, 0.0), radius: 0.3 };
|
|
let mut plane_count = 0i32;
|
|
world_collide_mover(&world, base, &mover_collide, default_query_filter(), &mut |_shape_id, planes| {
|
|
plane_count += planes.len() as i32;
|
|
true
|
|
});
|
|
|
|
// World ray cast from the left into the box, hitting the left face at relative x = -1
|
|
let ray_origin = offset_pos(base, vec3(-5.0, 0.0, 0.0));
|
|
let ray = world_cast_ray_closest(&world, ray_origin, vec3(10.0, 0.0, 0.0), default_query_filter());
|
|
let ray_hit = ray.hit;
|
|
let ray_rel_x = if ray.hit { sub_pos(ray.point, base).x } else { 0.0 };
|
|
|
|
// Direct shape ray cast against the same box
|
|
let shape_ray = shape_ray_cast(&world, shape_id, ray_origin, vec3(10.0, 0.0, 0.0));
|
|
let shape_ray_hit = shape_ray.hit;
|
|
let shape_ray_rel_x = if shape_ray.hit { sub_pos(shape_ray.point, base).x } else { 0.0 };
|
|
|
|
destroy_world(world);
|
|
|
|
QueryResult {
|
|
cast_hit,
|
|
cast_rel_x,
|
|
overlap_hit,
|
|
mover_fraction,
|
|
plane_count,
|
|
ray_hit,
|
|
ray_rel_x,
|
|
shape_ray_hit,
|
|
shape_ray_rel_x,
|
|
}
|
|
}
|
|
|
|
// The origin relative queries hit at the origin. (The C far-from-origin
|
|
// comparison is double precision only and is not ported.)
|
|
#[test]
|
|
fn large_world_query_test() {
|
|
let origin = run_queries(0.0);
|
|
ensure!(origin.cast_hit);
|
|
ensure!(origin.overlap_hit);
|
|
ensure!(origin.mover_fraction < 1.0);
|
|
ensure!(origin.plane_count > 0);
|
|
ensure_small!(origin.cast_rel_x + 1.0, 0.05);
|
|
ensure!(origin.ray_hit);
|
|
ensure_small!(origin.ray_rel_x + 1.0, 0.05);
|
|
ensure!(origin.shape_ray_hit);
|
|
ensure_small!(origin.shape_ray_rel_x + 1.0, 0.05);
|
|
}
|
|
|
|
// Port of the BOX3D_DOUBLE_PRECISION halves: a stack, a bullet, and the queries far from
|
|
// the origin must behave identically to the origin runs in double precision mode.
|
|
|
|
#[cfg(feature = "double-precision")]
|
|
#[test]
|
|
fn large_world_stack_far_test() {
|
|
let origin = run_stack(0.0);
|
|
ensure!(origin.sleep_step >= 0);
|
|
|
|
let far = run_stack(1.0e7);
|
|
ensure!(far.sleep_step >= 0);
|
|
|
|
// Sleeps on the same frame and lands in the same relative configuration
|
|
ensure!(far.sleep_step == origin.sleep_step);
|
|
for i in 0..STACK_COUNT {
|
|
ensure_small!(far.relative_positions[i].x - origin.relative_positions[i].x, 1.0e-3);
|
|
ensure_small!(far.relative_positions[i].y - origin.relative_positions[i].y, 1.0e-3);
|
|
ensure_small!(far.relative_positions[i].z - origin.relative_positions[i].z, 1.0e-3);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "double-precision")]
|
|
#[test]
|
|
fn large_world_bullet_far_test() {
|
|
// The blocking check is that the catch still holds far from the origin where the swept
|
|
// query box rounds back to float with large ULP.
|
|
let far_x = run_bullet(1.0e7);
|
|
ensure!(far_x < 5.0);
|
|
}
|
|
|
|
#[cfg(feature = "double-precision")]
|
|
#[test]
|
|
fn large_world_query_far_test() {
|
|
let origin = run_queries(0.0);
|
|
|
|
let far = run_queries(1.0e7);
|
|
ensure!(far.cast_hit);
|
|
ensure!(far.overlap_hit);
|
|
ensure!(far.mover_fraction < 1.0);
|
|
ensure!(far.plane_count > 0);
|
|
ensure!(far.ray_hit);
|
|
ensure!(far.shape_ray_hit);
|
|
|
|
ensure_small!(far.cast_rel_x - origin.cast_rel_x, 1.0e-3);
|
|
ensure_small!(far.mover_fraction - origin.mover_fraction, 1.0e-3);
|
|
ensure!(far.plane_count == origin.plane_count);
|
|
ensure_small!(far.ray_rel_x - origin.ray_rel_x, 1.0e-3);
|
|
ensure_small!(far.shape_ray_rel_x - origin.shape_ray_rel_x, 1.0e-3);
|
|
}
|