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

1146 lines
39 KiB
Rust

// Port of box3d/test/test_world.c (+ the scene helpers it uses from
// box3d/shared/benchmarks.c, stability.c, overflow_color.c).
//
// Adaptations from C (documented per test):
// - The Rust port has no global world registry: destroy_world consumes the
// World, so the C `b3World_IsValid(worldId) == false` checks after destroy
// are not expressible and are dropped.
// - Worker counts are ignored (always serial): world_set_worker_count is a
// no-op stub and world_get_worker_count always returns 1.
// - user data is u64 instead of void*.
use makepad_box3d::body::*;
use makepad_box3d::compound::create_compound;
use makepad_box3d::constants::GRAPH_COLOR_COUNT;
use makepad_box3d::ensure;
use makepad_box3d::ensure_small;
use makepad_box3d::hull::{create_cylinder, create_rock, make_box_hull, make_cube_hull, make_offset_box_hull};
use makepad_box3d::id::WorldId;
use makepad_box3d::math_functions::{compute_cos_sin, length, pos, vec3, Pos, Quat, Transform, Vec3, WorldTransform, PI};
use makepad_box3d::mesh::create_wave_mesh;
use makepad_box3d::physics_world::*;
use makepad_box3d::shape::*;
use makepad_box3d::test_utils::{random_vec3_uniform, set_random_seed};
use makepad_box3d::types::*;
fn world_id_of(world: &World) -> WorldId {
WorldId { index1: world.world_id + 1, generation: world.generation }
}
// This is a simple example of building and running a simulation
// using Box3D. Here we create a large ground box and a small dynamic box.
#[test]
fn hello_world() {
// Construct a world object, which will hold and simulate the rigid bodies.
let mut world_def = default_world_def();
world_def.gravity = vec3(0.0, -10.0, 0.0);
let mut world = create_world(&world_def);
ensure!(world_is_valid(&world, world_id_of(&world)));
// Define the ground body.
let mut ground_body_def = default_body_def();
ground_body_def.position = pos(0.0, -10.0, 0.0);
let ground_id = create_body(&mut world, &ground_body_def);
ensure!(body_is_valid(&world, ground_id));
// Define the ground box shape. The extents are the half-widths of the box.
let ground_box = make_box_hull(50.0, 10.0, 50.0);
// Add the box shape to the ground body.
let ground_shape_def = default_shape_def();
create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
// Define the dynamic body. We set its position and call the body factory.
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.position = pos(0.0, 4.0, 0.0);
let body_id = create_body(&mut world, &body_def);
// Define another box shape for our dynamic body.
let dynamic_box = make_cube_hull(1.0);
// Define the dynamic body shape
let mut shape_def = default_shape_def();
// Set the box density to be non-zero, so it will be dynamic.
shape_def.density = 1.0;
// Override the default friction.
shape_def.base_material.friction = 0.3;
// Add the shape to the body.
create_hull_shape(&mut world, body_id, &shape_def, &dynamic_box);
let time_step = 1.0 / 60.0;
let sub_step_count = 4;
let mut position = body_get_position(&world, body_id);
let mut rotation = body_get_rotation(&world, body_id);
// This is our little game loop.
for _ in 0..90 {
world_step(&mut world, time_step, sub_step_count);
position = body_get_position(&world, body_id);
rotation = body_get_rotation(&world, body_id);
}
destroy_world(world);
ensure_small!(position.y - 1.00, 0.01);
ensure_small!(rotation.v.x, 0.01);
ensure_small!(rotation.v.z, 0.01);
}
#[test]
fn empty_world() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
ensure!(world_is_valid(&world, world_id_of(&world)));
let time_step = 1.0 / 60.0;
let sub_step_count = 1;
for _ in 0..60 {
world_step(&mut world, time_step, sub_step_count);
}
destroy_world(world);
// C: ENSURE(b3World_IsValid(worldId) == false) — not expressible with an owned World.
}
const BODY_COUNT: usize = 10;
#[test]
fn destroy_all_bodies_world() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
let mut count = 0usize;
let mut creating = true;
let mut body_ids = [makepad_box3d::id::NULL_BODY_ID; BODY_COUNT];
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
let cube = make_cube_hull(0.5);
for _ in 0..(2 * BODY_COUNT + 10) {
if creating {
if count < BODY_COUNT {
body_ids[count] = create_body(&mut world, &body_def);
let shape_def = default_shape_def();
create_hull_shape(&mut world, body_ids[count], &shape_def, &cube);
count += 1;
} else {
creating = false;
}
} else if count > 0 {
destroy_body(&mut world, body_ids[count - 1]);
body_ids[count - 1] = makepad_box3d::id::NULL_BODY_ID;
count -= 1;
}
world_step(&mut world, 1.0 / 60.0, 3);
}
let counters = world_get_counters(&world);
ensure!(counters.body_count == 0);
destroy_world(world);
}
#[test]
fn test_is_valid() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
ensure!(world_is_valid(&world, world_id_of(&world)));
let body_def = default_body_def();
let body_id1 = create_body(&mut world, &body_def);
ensure!(body_is_valid(&world, body_id1) == true);
let body_id2 = create_body(&mut world, &body_def);
ensure!(body_is_valid(&world, body_id2) == true);
destroy_body(&mut world, body_id1);
ensure!(body_is_valid(&world, body_id1) == false);
destroy_body(&mut world, body_id2);
ensure!(body_is_valid(&world, body_id2) == false);
destroy_world(world);
// C checks world/body validity after world destroy — not expressible with an owned World.
}
const WORLD_COUNT: usize = 128 / 2; // B3_MAX_WORLDS / 2
#[test]
fn test_world_recycle() {
let count = 100;
for _ in 0..count {
let world_def = default_world_def();
let mut worlds: Vec<World> = Vec::with_capacity(WORLD_COUNT);
for _ in 0..WORLD_COUNT {
let mut world = create_world(&world_def);
ensure!(world_is_valid(&world, world_id_of(&world)));
let body_def = default_body_def();
create_body(&mut world, &body_def);
worlds.push(world);
}
for world in worlds.iter_mut() {
let time_step = 1.0 / 60.0;
let sub_step_count = 1;
for _ in 0..10 {
world_step(world, time_step, sub_step_count);
}
}
while let Some(world) = worlds.pop() {
destroy_world(world);
}
}
}
// This test is here to ensure all API functions link correctly.
#[test]
fn test_world_coverage() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
ensure!(world_is_valid(&world, world_id_of(&world)));
world_enable_sleeping(&mut world, true);
world_enable_sleeping(&mut world, false);
let mut flag = world_is_sleeping_enabled(&world);
ensure!(flag == false);
world_enable_continuous(&mut world, false);
world_enable_continuous(&mut world, true);
flag = world_is_continuous_enabled(&world);
ensure!(flag == true);
world_set_restitution_threshold(&mut world, 0.0);
world_set_restitution_threshold(&mut world, 2.0);
let mut value = world_get_restitution_threshold(&world);
ensure!(value == 2.0);
world_set_hit_event_threshold(&mut world, 0.0);
world_set_hit_event_threshold(&mut world, 100.0);
value = world_get_hit_event_threshold(&world);
ensure!(value == 100.0);
// C passes fn pointers with a NULL context; the port takes closures.
world_set_custom_filter_callback(&mut world, Some(Box::new(|_a, _b| true)));
world_set_pre_solve_callback(&mut world, Some(Box::new(|_a, _b, _point, _normal| false)));
let g = vec3(1.0, 2.0, 0.0);
world_set_gravity(&mut world, g);
let v = world_get_gravity(&world);
ensure!(v.x == g.x);
ensure!(v.y == g.y);
let explosion_def = default_explosion_def();
world_explode(&mut world, &explosion_def);
world_set_contact_tuning(&mut world, 10.0, 2.0, 4.0);
world_set_maximum_linear_speed(&mut world, 10.0);
value = world_get_maximum_linear_speed(&world);
ensure!(value == 10.0);
world_enable_warm_starting(&mut world, true);
flag = world_is_warm_starting_enabled(&world);
ensure!(flag == true);
let count = world_get_awake_body_count(&world);
ensure!(count == 0);
world_set_user_data(&mut world, 0xDEADBEEF);
let user_data = world_get_user_data(&world);
ensure!(user_data == 0xDEADBEEF);
world_step(&mut world, 1.0, 1);
destroy_world(world);
}
#[test]
fn test_sensor() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
// Wall from x = 1 to x = 2
let mut body_def = default_body_def();
body_def.body_type = BodyType::Static;
body_def.position = pos(1.5, 11.0, 0.0);
let wall_id = create_body(&mut world, &body_def);
let box_hull = make_box_hull(0.5, 10.0, 1.0);
let mut shape_def = default_shape_def();
shape_def.enable_sensor_events = true;
create_hull_shape(&mut world, wall_id, &shape_def, &box_hull);
// Bullet fired towards the wall
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.is_bullet = true;
body_def.gravity_scale = 0.0;
body_def.position = pos(7.39814, 4.0, 0.0);
body_def.linear_velocity = vec3(-20.0, 0.0, 0.0);
let bullet_id = create_body(&mut world, &body_def);
let mut shape_def = default_shape_def();
shape_def.is_sensor = true;
shape_def.enable_sensor_events = true;
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.1 };
create_sphere_shape(&mut world, bullet_id, &shape_def, &sphere);
let mut begin_count = 0;
let mut end_count = 0;
loop {
let time_step = 1.0 / 60.0;
let sub_step_count = 4;
world_step(&mut world, time_step, sub_step_count);
let bullet_pos = body_get_position(&world, bullet_id);
let events = world_get_sensor_events(&world);
if !events.begin_events.is_empty() {
begin_count += 1;
}
if !events.end_events.is_empty() {
end_count += 1;
}
if bullet_pos.x < -1.0 {
break;
}
}
destroy_world(world);
ensure!(begin_count == 1);
ensure!(end_count == 1);
}
#[test]
fn test_contact_events() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
// Static ground
let mut body_def = default_body_def();
body_def.body_type = BodyType::Static;
body_def.position = pos(0.0, -0.5, 0.0);
let ground_id = create_body(&mut world, &body_def);
let ground_box = make_box_hull(10.0, 0.5, 10.0);
let ground_shape_def = default_shape_def();
let ground_shape_id = create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
// Dynamic sphere dropped onto the ground; restitution causes it to bounce so we get end events
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.position = pos(0.0, 5.0, 0.0);
let sphere_body_id = create_body(&mut world, &body_def);
let mut shape_def = default_shape_def();
shape_def.density = 1.0;
shape_def.enable_contact_events = true;
shape_def.base_material.restitution = 0.6;
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
let sphere_shape_id = create_sphere_shape(&mut world, sphere_body_id, &shape_def, &sphere);
let mut begin_count = 0;
let mut end_count = 0;
let mut ids_checked = false;
for _ in 0..120 {
world_step(&mut world, 1.0 / 60.0, 4);
let (first_begin, begin_len, end_len) = {
let events = world_get_contact_events(&world);
(
events.begin_events.first().copied(),
events.begin_events.len(),
events.end_events.len(),
)
};
if begin_len > 0 && ids_checked == false {
let be = first_begin.unwrap();
let a_is_sphere = be.shape_id_a == sphere_shape_id;
let b_is_sphere = be.shape_id_b == sphere_shape_id;
let a_is_ground = be.shape_id_a == ground_shape_id;
let b_is_ground = be.shape_id_b == ground_shape_id;
ensure!((a_is_sphere && b_is_ground) || (a_is_ground && b_is_sphere));
ensure!(contact_is_valid(&world, be.contact_id));
ids_checked = true;
}
begin_count += begin_len;
end_count += end_len;
}
destroy_world(world);
ensure!(ids_checked);
ensure!(begin_count >= 1);
ensure!(end_count >= 1);
}
#[test]
fn test_hit_events() {
let mut world_def = default_world_def();
world_def.hit_event_threshold = 1.0;
let mut world = create_world(&world_def);
// Static ground
let mut body_def = default_body_def();
body_def.body_type = BodyType::Static;
body_def.position = pos(0.0, -0.5, 0.0);
let ground_id = create_body(&mut world, &body_def);
let ground_box = make_box_hull(10.0, 0.5, 10.0);
let ground_shape_def = default_shape_def();
create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
// Sphere driven into the ground fast enough to clear the hit threshold
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.gravity_scale = 0.0;
body_def.position = pos(0.0, 2.0, 0.0);
body_def.linear_velocity = vec3(0.0, -30.0, 0.0);
let sphere_body_id = create_body(&mut world, &body_def);
let mut shape_def = default_shape_def();
shape_def.density = 1.0;
shape_def.enable_hit_events = true;
shape_def.base_material.user_material_id = 7;
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
create_sphere_shape(&mut world, sphere_body_id, &shape_def, &sphere);
let mut hit_count = 0;
let mut captured_speed = 0.0;
let mut captured_material_a = 0u64;
let mut captured_material_b = 0u64;
let mut captured_normal = vec3(0.0, 0.0, 0.0);
for _ in 0..30 {
world_step(&mut world, 1.0 / 60.0, 4);
let events = world_get_contact_events(&world);
if !events.hit_events.is_empty() && hit_count == 0 {
let hit = events.hit_events[0];
captured_speed = hit.approach_speed;
captured_normal = hit.normal;
captured_material_a = hit.user_material_id_a;
captured_material_b = hit.user_material_id_b;
}
hit_count += events.hit_events.len();
}
destroy_world(world);
ensure!(hit_count >= 1);
ensure!(captured_speed > 1.0);
// Head-on vertical impact: normal lies along Y
ensure_small!(captured_normal.x, 0.01);
ensure_small!(captured_normal.z, 0.01);
// One side of the contact carries the sphere's user material
ensure!(captured_material_a == 7 || captured_material_b == 7);
}
// Hit-event material lookup must respect the compound child that participated in the
// contact. Two children with distinct userMaterialIds at separated positions, dropped
// sphere strikes one specifically.
#[test]
fn test_compound_hit_events() {
const HULL_MATERIAL_A: u64 = 11;
const HULL_MATERIAL_B: u64 = 22;
const SPHERE_MATERIAL: u64 = 99;
const HULL_CENTER_X: f32 = 3.0;
for side in 0..2 {
let expected_hull_material = if side == 0 { HULL_MATERIAL_A } else { HULL_MATERIAL_B };
let spawn_x = if side == 0 { -HULL_CENTER_X } else { HULL_CENTER_X };
let mut world_def = default_world_def();
world_def.hit_event_threshold = 1.0;
let mut world = create_world(&world_def);
// Build a compound with two hulls at opposite x positions, distinct userMaterialIds
let box_a = make_box_hull(1.0, 1.0, 1.0);
let box_b = make_box_hull(1.0, 1.0, 1.0);
let mut mat_a = default_surface_material();
mat_a.user_material_id = HULL_MATERIAL_A;
let mut mat_b = default_surface_material();
mat_b.user_material_id = HULL_MATERIAL_B;
let compound_def = CompoundDef {
hulls: vec![
CompoundHullDef {
hull: box_a.clone(),
transform: Transform { p: vec3(-HULL_CENTER_X, 0.0, 0.0), q: Quat::IDENTITY },
material: mat_a,
},
CompoundHullDef {
hull: box_b.clone(),
transform: Transform { p: vec3(HULL_CENTER_X, 0.0, 0.0), q: Quat::IDENTITY },
material: mat_b,
},
],
..Default::default()
};
let compound = create_compound(&compound_def);
// Static body holds the compound
let mut body_def = default_body_def();
body_def.body_type = BodyType::Static;
let compound_body_id = create_body(&mut world, &body_def);
let compound_shape_def = default_shape_def();
create_compound_shape(&mut world, compound_body_id, &compound_shape_def, &compound);
// Sphere driven straight down onto the chosen child
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.gravity_scale = 0.0;
body_def.position = pos(spawn_x, 3.0, 0.0);
body_def.linear_velocity = vec3(0.0, -30.0, 0.0);
let sphere_body_id = create_body(&mut world, &body_def);
let mut sphere_shape_def = default_shape_def();
sphere_shape_def.density = 1.0;
sphere_shape_def.enable_hit_events = true;
sphere_shape_def.base_material.user_material_id = SPHERE_MATERIAL;
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
create_sphere_shape(&mut world, sphere_body_id, &sphere_shape_def, &sphere);
let mut hit_count = 0;
let mut captured_material_a = 0u64;
let mut captured_material_b = 0u64;
for _ in 0..30 {
world_step(&mut world, 1.0 / 60.0, 4);
let events = world_get_contact_events(&world);
if !events.hit_events.is_empty() && hit_count == 0 {
let hit = events.hit_events[0];
captured_material_a = hit.user_material_id_a;
captured_material_b = hit.user_material_id_b;
}
hit_count += events.hit_events.len();
}
destroy_world(world);
ensure!(hit_count >= 1);
// Sphere material on one side
ensure!(captured_material_a == SPHERE_MATERIAL || captured_material_b == SPHERE_MATERIAL);
// Struck compound child's material on the other side.
ensure!(captured_material_a == expected_hull_material || captured_material_b == expected_hull_material);
}
}
struct JunkyardData {
pusher_id: makepad_box3d::id::BodyId,
degrees: f32,
radius: f32,
}
// Port of CreateJunkyard from box3d/shared/benchmarks.c using the debug body
// count (2 layers instead of 24) — the C test build uses the same reduction.
fn create_junkyard(world: &mut World) -> JunkyardData {
let ground_id;
{
let mut body_def = default_body_def();
body_def.position.y = -1.0;
ground_id = create_body(world, &body_def);
}
{
let shape_def = default_shape_def();
{
let box_hull = make_box_hull(120.0, 1.0, 120.0);
create_hull_shape(world, ground_id, &shape_def, &box_hull);
}
{
let box_hull = make_offset_box_hull(1.0, 8.0, 50.0, vec3(-50.0, 8.0, 0.0));
create_hull_shape(world, ground_id, &shape_def, &box_hull);
}
{
let box_hull = make_offset_box_hull(1.0, 8.0, 50.0, vec3(50.0, 8.0, 0.0));
create_hull_shape(world, ground_id, &shape_def, &box_hull);
}
{
let box_hull = make_offset_box_hull(50.0, 8.0, 1.0, vec3(0.0, 8.0, -50.0));
create_hull_shape(world, ground_id, &shape_def, &box_hull);
}
{
let box_hull = make_offset_box_hull(50.0, 8.0, 1.0, vec3(0.0, 8.0, 50.0));
create_hull_shape(world, ground_id, &shape_def, &box_hull);
}
}
{
let rock_hull = create_rock(1.5);
let count = 2; // BENCHMARK_DEBUG variant
let height = 24.0;
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
let shape_def = default_shape_def();
for y in 0..count {
for x in 0..=20 {
for z in 0..=20 {
let px = -40.0 + 4.0 * x as f32;
let py = 4.0 * y as f32 + height + 1.0;
let pz = -40.0 + 4.0 * z as f32; body_def.position = pos(px, py, pz);
let body_id = create_body(world, &body_def);
create_hull_shape(world, body_id, &shape_def, &rock_hull);
}
}
}
}
let radius = 35.0;
let m_height = 24.0;
let hull = create_cylinder(m_height, 4.0, 0.0, 16);
let mut body_def = default_body_def();
body_def.body_type = BodyType::Kinematic;
body_def.position = pos(radius, 0.0, 0.0);
let pusher_id = create_body(world, &body_def);
let shape_def = default_shape_def();
create_hull_shape(world, pusher_id, &shape_def, &hull);
JunkyardData { pusher_id, degrees: 0.0, radius }
}
// Port of StepJunkyard: note the C version only drives the kinematic pusher; it
// does not step the world.
fn step_junkyard(world: &mut World, data: &mut JunkyardData) {
let time_step = 1.0 / 60.0;
let omega = -6.0;
data.degrees += omega * time_step;
let cs = compute_cos_sin(data.degrees * PI / 180.0);
let r = data.radius;
let target_pos: Pos = pos(r * cs.cosine, 0.0, r * cs.sine);
let target = WorldTransform { p: target_pos, q: Quat::IDENTITY };
body_set_target_transform(world, data.pusher_id, target, time_step, false);
}
// Adapted: the Rust port is always serial. world_set_worker_count is a no-op
// stub and world_get_worker_count always returns 1, so the C expectations of
// 4 / B3_MAX_WORKERS collapse to 1.
#[test]
fn test_set_worker_count() {
let mut world_def = default_world_def();
world_def.worker_count = 1;
let mut world = create_world(&world_def);
ensure!(world_get_worker_count(&world) == 1);
let mut junkyard = create_junkyard(&mut world);
step_junkyard(&mut world, &mut junkyard);
world_set_worker_count(&mut world, 4);
ensure!(world_get_worker_count(&world) == 1);
step_junkyard(&mut world, &mut junkyard);
world_set_worker_count(&mut world, 4);
ensure!(world_get_worker_count(&world) == 1);
step_junkyard(&mut world, &mut junkyard);
world_set_worker_count(&mut world, 0);
ensure!(world_get_worker_count(&world) == 1);
step_junkyard(&mut world, &mut junkyard);
world_set_worker_count(&mut world, -5);
ensure!(world_get_worker_count(&world) == 1);
step_junkyard(&mut world, &mut junkyard);
world_set_worker_count(&mut world, makepad_box3d::constants::MAX_WORKERS as i32 + 10);
ensure!(world_get_worker_count(&world) == 1);
step_junkyard(&mut world, &mut junkyard);
destroy_world(world);
}
// This tests continuous collision and mesh contact stability.
// Port of CreateMeshDrop from box3d/shared/stability.c.
#[test]
fn test_mesh_drop() {
let world_def = default_world_def();
// C requests 4 workers; the port is always serial.
let mut world = create_world(&world_def);
// CreateMeshDrop
{
let body_def = default_body_def();
let ground_id = create_body(&mut world, &body_def);
let grid_count = 40;
let cell_width = 1.0;
let row_hz = 0.1;
let column_hz = 0.2;
let ground_amplitude = 0.5;
let mesh = create_wave_mesh(grid_count, grid_count, cell_width, ground_amplitude, row_hz, column_hz);
let mut shape_def = default_shape_def();
shape_def.filter.category_bits = 1;
create_mesh_shape(&mut world, ground_id, &shape_def, &mesh, Vec3::ONE);
}
{
let box_hull = make_box_hull(0.02, 0.2, 0.04);
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
let mut shape_def = default_shape_def();
shape_def.base_material.rolling_resistance = 0.1;
// Don't allow shapes to collide with each other.
shape_def.filter.category_bits = 2;
shape_def.filter.mask_bits = 1;
set_random_seed(3963634789);
let grid_count = 32; // MESH_DROP_GRID_COUNT
for i in 0..grid_count {
for j in 0..grid_count {
let linear_velocity = random_vec3_uniform(-1.0, 1.0);
let angular_velocity = random_vec3_uniform(-5.0, 5.0);
body_def.position = pos(
0.5 * (i as f32 - 0.5 * grid_count as f32),
5.0,
0.5 * (j as f32 - 0.5 * grid_count as f32),
);
body_def.linear_velocity = linear_velocity;
body_def.angular_velocity = angular_velocity;
let body_id = create_body(&mut world, &body_def);
create_hull_shape(&mut world, body_id, &shape_def, &box_hull);
}
}
}
let time_step = 1.0 / 60.0;
let mut step_index = 0;
let step_limit = 400;
while step_index < step_limit {
let sub_step_count = 4;
world_step(&mut world, time_step, sub_step_count);
let move_count = world_get_body_events(&world).move_events.len();
if move_count == 0 {
// All bodies sleeping
break;
}
step_index += 1;
}
println!(" test_mesh_drop step_index = {}", step_index);
destroy_world(world);
ensure!(step_index < step_limit);
}
// Verifies the overflow solver path. The scene puts more dyn-dyn contacts on a
// single hub body than there are dynamic graph colors, so several land in the
// overflow color. Port of CreateOverflowColorPile from box3d/shared/overflow_color.c.
#[test]
fn test_overflow_color_pile() {
const RING_COUNT: usize = 5;
const PER_RING: usize = 5;
let world_def = default_world_def();
let mut world = create_world(&world_def);
// Static ground (top surface at y = 0)
{
let mut body_def = default_body_def();
body_def.position = pos(0.0, -1.0, 0.0);
let ground_id = create_body(&mut world, &body_def);
let box_hull = make_box_hull(20.0, 1.0, 20.0);
let shape_def = default_shape_def();
create_hull_shape(&mut world, ground_id, &shape_def, &box_hull);
}
// Tall, heavy hub.
let hub_half_x = 0.5f32;
let hub_half_y = 2.5f32;
let hub_half_z = 0.5f32;
{
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.position = pos(0.0, hub_half_y, 0.0);
let hub_id = create_body(&mut world, &body_def);
let box_hull = make_box_hull(hub_half_x, hub_half_y, hub_half_z);
let mut shape_def = default_shape_def();
shape_def.density = 50.0;
create_hull_shape(&mut world, hub_id, &shape_def, &box_hull);
}
// Neighbors: vertical rings around the hub, each box slightly overlapping
// the hub so a contact exists on the very first step.
let neighbor_half = 0.2f32;
let ring_radius = hub_half_x + neighbor_half - 0.03;
let neighbor_box = make_box_hull(neighbor_half, neighbor_half, neighbor_half);
let neighbor_shape = default_shape_def();
let ring_spacing = 0.5f32;
let base_y = neighbor_half + 0.05;
let _ = hub_half_z;
for ring in 0..RING_COUNT {
let y = base_y + ring_spacing * ring as f32;
// Offset alternate rings by half a slot.
let theta_offset = if (ring & 1) != 0 { PI / PER_RING as f32 } else { 0.0 };
for slot in 0..PER_RING {
let theta = theta_offset + (2.0 * PI * slot as f32) / PER_RING as f32;
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.position = pos(ring_radius * theta.cos(), y, ring_radius * theta.sin());
let body_id = create_body(&mut world, &body_def);
create_hull_shape(&mut world, body_id, &neighbor_shape, &neighbor_box);
}
}
let time_step = 1.0 / 60.0;
let sub_step_count = 4;
let step_count = 10;
for _ in 0..step_count {
world_step(&mut world, time_step, sub_step_count);
}
// Confirm the scene actually populated the overflow color.
let counters = world_get_counters(&world);
let overflow_contacts = counters.color_counts[GRAPH_COLOR_COUNT - 1];
destroy_world(world);
ensure!(overflow_contacts > 0);
}
// b3Body_EnableSleep must sync bodySim/bodyState flags; the flag-sync assertion
// in validate_solver_sets fires on the next step otherwise.
#[test]
fn enable_sleep_flag_sync_test() {
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::Dynamic;
body_def.enable_sleep = false;
let body_id = create_body(&mut world, &body_def);
ensure!(body_is_sleep_enabled(&world, body_id) == false);
body_enable_sleep(&mut world, body_id, true);
ensure!(body_is_sleep_enabled(&world, body_id) == true);
world_step(&mut world, 1.0 / 60.0, 4);
destroy_world(world);
}
// b3Body_SetBullet must not drift against b3SyncBodyFlags.
#[test]
fn set_bullet_drift_test() {
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::Dynamic;
body_def.is_bullet = false;
let body_id = create_body(&mut world, &body_def);
ensure!(body_is_bullet(&world, body_id) == false);
body_set_bullet(&mut world, body_id, true);
ensure!(body_is_bullet(&world, body_id) == true);
let mut locks = MotionLocks::default();
locks.linear_x = true;
body_set_motion_locks(&mut world, body_id, locks);
ensure!(body_is_bullet(&world, body_id) == true);
}
{
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.is_bullet = true;
let body_id = create_body(&mut world, &body_def);
ensure!(body_is_bullet(&world, body_id) == true);
body_set_bullet(&mut world, body_id, false);
ensure!(body_is_bullet(&world, body_id) == false);
let mut locks = MotionLocks::default();
locks.linear_x = true;
body_set_motion_locks(&mut world, body_id, locks);
ensure!(body_is_bullet(&world, body_id) == false);
}
destroy_world(world);
}
// Regression: b3Body_EnableSleep used to leak the world lock on a no-op change.
#[test]
fn enable_sleep_noop_unlock_test() {
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::Dynamic;
body_def.enable_sleep = true;
let body_id = create_body(&mut world, &body_def);
// No-op: enableSleep is already true. Must not leak the world lock.
body_enable_sleep(&mut world, body_id, true);
// Would assert in the unlocked-world guard if the lock had leaked.
body_enable_sleep(&mut world, body_id, false);
ensure!(body_is_sleep_enabled(&world, body_id) == false);
destroy_world(world);
}
#[test]
fn enable_contact_recycling_test() {
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::Dynamic;
// Default is enabled
let body_a = create_body(&mut world, &body_def);
ensure!(body_is_contact_recycling_enabled(&world, body_a) == true);
body_enable_contact_recycling(&mut world, body_a, false);
ensure!(body_is_contact_recycling_enabled(&world, body_a) == false);
body_enable_contact_recycling(&mut world, body_a, true);
ensure!(body_is_contact_recycling_enabled(&world, body_a) == true);
// Per-def opt-out at creation
body_def.enable_contact_recycling = false;
let body_b = create_body(&mut world, &body_def);
ensure!(body_is_contact_recycling_enabled(&world, body_b) == false);
// Stepping after toggling must not trip the flag-sync validator
world_step(&mut world, 1.0 / 60.0, 4);
destroy_world(world);
}
// Identical hull data is shared through a reference counted world database.
#[test]
fn test_hull_database() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
let box_hull = make_box_hull(0.5, 0.5, 0.5);
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
let body_a = create_body(&mut world, &body_def);
let body_b = create_body(&mut world, &body_def);
let shape_def = default_shape_def();
// Two shapes built from identical data share one owned copy in the world database.
let shape_a = create_hull_shape(&mut world, body_a, &shape_def, &box_hull);
let shape_b = create_hull_shape(&mut world, body_b, &shape_def, &box_hull);
let got_a = shape_get_hull(&world, shape_a);
let got_b = shape_get_hull(&world, shape_b);
// Both shapes point at the single shared copy
ensure!(std::sync::Arc::ptr_eq(&got_a, &got_b));
// C also checks the shared copy is not the caller's stack hull. The Rust
// port stores the caller's Arc on first insert by design (Arc handles the
// lifetime), so that check does not apply.
// A box built independently must de-duplicate to the same shared copy.
let box2 = make_box_hull(0.5, 0.5, 0.5);
let body_c = create_body(&mut world, &body_def);
let shape_c = create_hull_shape(&mut world, body_c, &shape_def, &box2);
ensure!(std::sync::Arc::ptr_eq(&shape_get_hull(&world, shape_c), &got_a));
destroy_shape(&mut world, shape_c, true);
// Setting a shape's hull to its own sole shared copy must not free it mid update.
let box3 = make_box_hull(0.3, 0.3, 0.3);
let body_d = create_body(&mut world, &body_def);
let shape_d = create_hull_shape(&mut world, body_d, &shape_def, &box3);
let got_d = shape_get_hull(&world, shape_d);
shape_set_hull(&mut world, shape_d, &got_d);
ensure!(std::sync::Arc::ptr_eq(&shape_get_hull(&world, shape_d), &got_d));
destroy_shape(&mut world, shape_d, true);
// Releasing one reference keeps the other alive
destroy_shape(&mut world, shape_a, true);
let still_b = shape_get_hull(&world, shape_b);
ensure!(std::sync::Arc::ptr_eq(&still_b, &got_b));
destroy_shape(&mut world, shape_b, true);
// World destroy asserts the database drained to zero references
destroy_world(world);
}
struct ExplosionResult {
linear_velocity: Vec3,
angular_velocity: Vec3,
}
// Explode just off the +x side of a centered sphere and capture the impulse it
// receives. The result must not depend on how far the body sits from the origin.
fn run_explosion(base: Pos) -> ExplosionResult {
let mut world_def = default_world_def();
world_def.gravity = Vec3::ZERO;
let mut world = create_world(&world_def);
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.position = base;
let body_id = create_body(&mut world, &body_def);
let sphere = Sphere { center: Vec3::ZERO, radius: 1.0 };
let shape_def = default_shape_def();
create_sphere_shape(&mut world, body_id, &shape_def, &sphere);
// Blast sits 3 units along +x, so the body is pushed back along -x
let mut explosion_def = default_explosion_def();
explosion_def.position = makepad_box3d::math_functions::offset_pos(base, vec3(3.0, 0.0, 0.0));
explosion_def.radius = 5.0;
explosion_def.falloff = 0.0;
explosion_def.impulse_per_area = 10.0;
world_explode(&mut world, &explosion_def);
let result = ExplosionResult {
linear_velocity: body_get_linear_velocity(&world, body_id),
angular_velocity: body_get_angular_velocity(&world, body_id),
};
destroy_world(world);
result
}
#[test]
fn test_explosion() {
let origin = run_explosion(makepad_box3d::math_functions::POS_ZERO);
// Pushed away from the blast along -x. A centered sphere has no transverse
// or angular component.
ensure!(origin.linear_velocity.x < -1.0e-4);
ensure_small!(origin.linear_velocity.y, 1.0e-6);
ensure_small!(origin.linear_velocity.z, 1.0e-6);
ensure_small!(length(origin.angular_velocity), 1.0e-6);
// The same blast far from the origin must produce the same impulse.
let far = run_explosion(pos(1.0e7, 1.0e7, 1.0e7));
ensure_small!(far.linear_velocity.x - origin.linear_velocity.x, 1.0e-5);
ensure_small!(far.linear_velocity.y - origin.linear_velocity.y, 1.0e-5);
ensure_small!(far.linear_velocity.z - origin.linear_velocity.z, 1.0e-5);
}
// Ensure correct move events from bodies involved in CCD.
#[test]
fn test_continuous_move_event() {
let world_def = default_world_def();
let mut world = create_world(&world_def);
world_enable_continuous(&mut world, true);
// Thin static wall, near face at x = 0.1
let mut body_def = default_body_def();
body_def.body_type = BodyType::Static;
body_def.position = pos(0.0, 0.0, 0.0);
let wall_id = create_body(&mut world, &body_def);
let wall_box = make_box_hull(0.1, 5.0, 5.0);
let shape_def = default_shape_def();
create_hull_shape(&mut world, wall_id, &shape_def, &wall_box);
// Fast dynamic sphere fired at the wall.
let mut body_def = default_body_def();
body_def.body_type = BodyType::Dynamic;
body_def.gravity_scale = 0.0;
body_def.position = pos(3.0, 0.0, 0.0);
body_def.linear_velocity = vec3(-30.0, 0.0, 0.0);
let ball_id = create_body(&mut world, &body_def);
let mut shape_def = default_shape_def();
shape_def.density = 1.0;
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.25 };
create_sphere_shape(&mut world, ball_id, &shape_def, &sphere);
let time_step = 1.0 / 60.0;
let sub_step_count = 4;
let mut have_move = false;
for _ in 0..30 {
world_step(&mut world, time_step, sub_step_count);
let xf = body_get_transform(&world, ball_id);
let events = world_get_body_events(&world);
for event in events.move_events {
if event.body_id != ball_id {
continue;
}
have_move = true;
// The move event must carry the same pose the body reports, CCD rewind included
ensure!(event.transform.p.x == xf.p.x);
ensure!(event.transform.p.y == xf.p.y);
ensure!(event.transform.p.z == xf.p.z);
ensure!(event.transform.q.v.x == xf.q.v.x);
ensure!(event.transform.q.v.y == xf.q.v.y);
ensure!(event.transform.q.v.z == xf.q.v.z);
ensure!(event.transform.q.s == xf.q.s);
}
}
ensure!(have_move == true);
// Tunnel check
let final_pos = body_get_position(&world, ball_id);
ensure!(0.2 < final_pos.x && final_pos.x < 0.8);
destroy_world(world);
}