makepad/libs/box3d/tests/test_determinism.rs
Admin be21d627a0 box3d: opt-in broad-phase hybrid (parallel BVTT) — single-thread washer -19%, default off
WorldDef.enable_broad_phase_hybrid (default false; -bp=0/1 bench toggle):
an adaptive batch broad phase for high-churn scenes. When
move_count*4 > proxy_count, replaces the per-moved-proxy tree queries
(8k proxies x 3 root-descents on washer) with three BVTT self/cross
traversals (dynamic self + dynamic x static + dynamic x kinematic) that
share the upper-tree descent, plus an O(n) bottom-up refit instead of
the median rebuild. Both traversal and candidate-filter are parallelized
across the task system (per-worker buffers → merge → canonical sort by
(shape_a,shape_b,child) → deterministic contact creation).

Correctness: a #[cfg(debug_assertions)] SET-equality assertion (batch
candidate set == per-mover set) runs in every test and never fires — the
proof the BVTT finds identical contacts (the hash can't prove it since
creation order legitimately re-baselines). New test
determinism_broad_phase_hybrid_across_worker_counts. OFF hash 0x61E35C31
bit-identical; ON hash 0xBE99C5F7 identical across workers 1/2/4 +
external tasks. 180/186/180/180 tests, zero warnings, profile retrained.

Single-threaded washer -18.7% (17715 vs 21780, broad phase -51%) — beats
C (20661), within ~6% of Rapier (16844). DEFAULT OFF because it regresses
multi-threaded (washer w8 +52%): the batch materializes ~40-50k
candidates/step and serially merges+sorts them (a fundamental floor the
inline per-mover path avoids by filtering in the query callback), so at
w8 the parallel per-mover queries win. Cannot be worker-gated (would
break cross-worker determinism). Correct, deterministic, zero-cost when
off — an opt-in single-threaded accelerator for churn-heavy scenes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 02:48:47 +02:00

760 lines
31 KiB
Rust

// Port of box3d/test/test_determinism.c + the scenario scaffolding from
// box3d/shared/determinism.c and box3d/shared/human.c (falling ragdolls on a
// grid mesh + torus mesh).
//
// DEVIATION from C: the C test compares the settled world hash against a
// hard-coded EXPECTED_HASH (0x50313037 float build) captured from the C
// binary. The Rust port preserves the C float operation order but cannot
// promise bit-equality with a C build (different IEEE remainder
// implementation, different code generation), so this port runs the identical
// scenario TWICE from scratch and requires both runs to produce the same
// sleep step and the same hash (internal determinism). The hash is printed so
// it can be tracked across Rust builds/platforms. The C worker-count loop
// (1..6 workers must agree) collapses to the serial port's single
// configuration.
use makepad_box3d::body::*;
use makepad_box3d::core::{hash, HASH_INIT};
use makepad_box3d::ensure;
use makepad_box3d::id::{BodyId, JointId, NULL_BODY_ID, NULL_JOINT_ID};
use makepad_box3d::joint::*;
use makepad_box3d::math_functions::{
normalize_quat, offset_pos, quat, vec3, Pos, Transform, Vec2, Vec3, WorldTransform,
DEG_TO_RAD,
};
use makepad_box3d::mesh::{create_grid_mesh, create_torus_mesh};
use makepad_box3d::physics_world::*;
use makepad_box3d::shape::{create_capsule_shape, create_mesh_shape};
use makepad_box3d::types::*;
// ---------------------------------------------------------------------------
// human.c scaffolding (CreateHuman only — the parts the scenario uses)
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, PartialEq)]
enum BoneJointType {
None,
Revolute,
Spherical,
}
const BONE_PELVIS: usize = 0;
const BONE_SPINE_01: usize = 1;
const BONE_SPINE_02: usize = 2;
const BONE_SPINE_03: usize = 3;
const BONE_NECK: usize = 4;
const BONE_HEAD: usize = 5;
const BONE_THIGH_L: usize = 6;
const BONE_CALF_L: usize = 7;
const BONE_THIGH_R: usize = 8;
const BONE_CALF_R: usize = 9;
const BONE_UPPER_ARM_L: usize = 10;
const BONE_LOWER_ARM_L: usize = 11;
const BONE_UPPER_ARM_R: usize = 12;
const BONE_LOWER_ARM_R: usize = 13;
const BONE_COUNT: usize = 14;
struct Bone {
body_id: BodyId,
joint_id: JointId,
local_frame_a: Transform,
local_frame_b: Transform,
reference_frame: Transform,
joint_type: BoneJointType,
swing_limit: f32,
twist_limit: Vec2,
joint_friction: f32,
parent_index: i32,
}
impl Default for Bone {
fn default() -> Self {
Bone {
body_id: NULL_BODY_ID,
joint_id: NULL_JOINT_ID,
local_frame_a: Transform::IDENTITY,
local_frame_b: Transform::IDENTITY,
reference_frame: Transform::IDENTITY,
joint_type: BoneJointType::None,
swing_limit: 0.0,
twist_limit: Vec2 { x: 0.0, y: 0.0 },
joint_friction: 1.0,
parent_index: -1,
}
}
}
#[derive(Default)]
struct Human {
bones: Vec<Bone>,
filter_joints: Vec<JointId>,
}
fn transform(px: f32, py: f32, pz: f32, qx: f32, qy: f32, qz: f32, qs: f32) -> Transform {
Transform { p: vec3(px, py, pz), q: quat(vec3(qx, qy, qz), qs) }
}
// Port of CreateHuman (human.c). colorize is always false in the determinism
// scenario so the color plumbing is dropped; userData is unused (0).
fn create_human(
world: &mut World,
position: Pos,
friction_torque: f32,
hertz: f32,
damping_ratio: f32,
group_index: i32,
) -> Human {
let mut human = Human::default();
for _ in 0..BONE_COUNT {
human.bones.push(Bone::default());
}
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.2;
{
let bone = &mut human.bones[BONE_PELVIS];
bone.parent_index = -1;
body_def.name = "pelvis".to_string();
bone.reference_frame = transform(0.0, 0.932087, -0.051708, 0.739169, 0.0, 0.0, 0.673520);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
let capsule = Capsule { center1: vec3(0.07, 0.0, -0.08), center2: vec3(-0.07, 0.0, -0.08), radius: 0.13 };
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_PELVIS].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_SPINE_01];
bone.parent_index = BONE_PELVIS as i32;
body_def.name = "spine_01".to_string();
bone.reference_frame = transform(0.0, 1.113505, -0.03481, 0.739973, 0.0, 0.0, 0.672637);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(0.000000, 0.000000, -0.182204, -0.999999, 0.000000, -0.000000, 0.001194);
bone.local_frame_b = transform(0.000000, 0.000000, -0.007736, -1.000000, 0.000000, -0.000000, 0.000000);
bone.swing_limit = 25.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -15.0 * DEG_TO_RAD, y: 15.0 * DEG_TO_RAD };
let capsule = Capsule { center1: vec3(0.06, -0.0, -0.052264), center2: vec3(-0.06, 0.0, -0.052264), radius: 0.12 };
shape_def.filter.group_index = -group_index;
let body_id = create_body(world, &body_def);
human.bones[BONE_SPINE_01].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_SPINE_02];
bone.parent_index = BONE_SPINE_01 as i32;
// C: bodyDef.name assignment is commented out; the previous name sticks.
bone.reference_frame = transform(0.0, 1.194336, -0.027087, 0.703611, 0.0, 0.0, 0.710586);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(0.000000, -0.000000, -0.088935, -0.998619, -0.000000, 0.000000, -0.052540);
bone.local_frame_b = transform(-0.000000, 0.000000, -0.008199, -1.000000, 0.000000, -0.000000, 0.000000);
bone.swing_limit = 25.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -15.0 * DEG_TO_RAD, y: 15.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.08, -0.015133, -0.091801),
center2: vec3(-0.08, -0.015133, -0.091801),
radius: 0.10,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_SPINE_02].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_SPINE_03];
bone.parent_index = BONE_SPINE_02 as i32;
body_def.name = "spine_03".to_string();
bone.reference_frame = transform(-0.0, 1.31043, -0.028232, 0.669856, 0.000001, -0.000001, 0.742491);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(-0.000000, 0.000000, -0.124298, -0.998921, 0.000001, -0.000001, -0.046434);
bone.local_frame_b = transform(0.000000, 0.000000, 0.000000, -1.000000, 0.000000, -0.000001, 0.000000);
bone.swing_limit = 15.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -10.0 * DEG_TO_RAD, y: 10.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.11, -0.039753, -0.13),
center2: vec3(-0.11, -0.039753, -0.13),
radius: 0.145,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_SPINE_03].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_NECK];
bone.parent_index = BONE_SPINE_03 as i32;
body_def.name = "neck".to_string();
bone.reference_frame = transform(0.0, 1.575582, -0.055837, 0.879922, 0.0, 0.0, 0.475118);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(0.000001, -0.000259, -0.266585, -0.942192, -0.000001, 0.000000, 0.335074);
bone.local_frame_b = transform(0.000000, 0.000000, 0.000000, -1.000000, 0.000000, -0.000001, 0.000000);
bone.swing_limit = 45.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -15.0 * DEG_TO_RAD, y: 15.0 * DEG_TO_RAD };
bone.joint_friction = 0.8;
let capsule = Capsule {
center1: vec3(-0.000001, -0.0, -0.02),
center2: vec3(0.0, -0.005, -0.08),
radius: 0.07,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_NECK].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_HEAD];
bone.parent_index = BONE_NECK as i32;
body_def.name = "head".to_string();
bone.reference_frame = transform(0.0, 1.653348, -0.003241, 0.750288, 0.0, 0.0, 0.661111);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(0.000000, 0.001321, -0.093873, -0.974301, -0.000000, -0.000000, -0.225251);
bone.local_frame_b = transform(0.000000, 0.001268, -0.005104, -1.000000, 0.000000, -0.00000, 0.000000);
bone.swing_limit = 15.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -15.0 * DEG_TO_RAD, y: 15.0 * DEG_TO_RAD };
bone.joint_friction = 0.4;
let capsule = Capsule {
center1: vec3(-0.000001, 0.016892, -0.05869),
center2: vec3(0.0, -0.003629, -0.115072),
radius: 0.0975,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_HEAD].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_THIGH_L];
bone.parent_index = BONE_PELVIS as i32;
body_def.name = "thigh_l".to_string();
bone.reference_frame = transform(0.090416, 0.986104, -0.035090, -0.703287, -0.070715, 0.053866, 0.705327);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(0.05, 0.011537, -0.055325, -0.714896, -0.022305, -0.698361, -0.026790);
bone.local_frame_b = transform(0.0, 0.0, 0.0, -0.002064, 0.758987, 0.017046, 0.650880);
bone.swing_limit = 10.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -60.0 * DEG_TO_RAD, y: 40.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.023719, 0.006008, -0.039068),
center2: vec3(-0.064492, -0.004664, -0.424718),
radius: 0.09,
};
shape_def.filter.group_index = -group_index;
let body_id = create_body(world, &body_def);
human.bones[BONE_THIGH_L].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_CALF_L];
bone.parent_index = BONE_THIGH_L as i32;
body_def.name = "calf_l".to_string();
bone.reference_frame = transform(0.101198, 0.527027, -0.037374, -0.653328, -0.066860, 0.058582, 0.751838);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Revolute;
bone.local_frame_a = transform(-0.069989, 0.000253, -0.453844, -0.000677, 0.760087, 0.105674, 0.641171);
bone.local_frame_b = transform(0.0, 0.0, 0.0, -0.044589, 0.765540, 0.053368, 0.639619);
bone.twist_limit = Vec2 { x: -5.0 * DEG_TO_RAD, y: 45.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.001778, 0.0, 0.009841),
center2: vec3(-0.078577, 0.014707, -0.41816),
radius: 0.075,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_CALF_L].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_THIGH_R];
bone.parent_index = BONE_PELVIS as i32;
body_def.name = "thigh_r".to_string();
bone.reference_frame = transform(-0.090416, 0.986104, -0.03509, -0.703287, 0.070715, -0.053865, 0.705326);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(-0.05, 0.011537, -0.055326, -0.039089, -0.714094, 0.043177, 0.697623);
bone.local_frame_b = transform(0.0, 0.0, 0.0, 0.758805, -0.019886, -0.651012, -0.001759);
bone.swing_limit = 10.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -30.0 * DEG_TO_RAD, y: 60.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(-0.023719, 0.006008, -0.039068),
center2: vec3(0.064492, -0.004664, -0.424718),
radius: 0.09,
};
shape_def.filter.group_index = -group_index;
let body_id = create_body(world, &body_def);
human.bones[BONE_THIGH_R].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_CALF_R];
bone.parent_index = BONE_THIGH_R as i32;
body_def.name = "calf_r".to_string();
bone.reference_frame = transform(-0.101198, 0.527027, -0.037373, -0.653327, 0.06686, -0.058582, 0.751839);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Revolute;
bone.local_frame_a = transform(0.069988, 0.000253, -0.453844, 0.760086, -0.000675, -0.641171, -0.105676);
bone.local_frame_b = transform(0.0, 0.0, 0.0, 0.765540, -0.044589, -0.639619, -0.053368);
bone.twist_limit = Vec2 { x: -45.0 * DEG_TO_RAD, y: 5.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(-0.001820, 0.0, 0.010071),
center2: vec3(0.077883, 0.014825, -0.418047),
radius: 0.075,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_CALF_R].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_UPPER_ARM_L];
bone.parent_index = BONE_SPINE_03 as i32;
body_def.name = "upper_arm_l".to_string();
bone.reference_frame = transform(0.20378, 1.484275, -0.115897, 0.143082, 0.695980, -0.690130, 0.13733);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(0.203780, -0.069369, -0.181921, -0.278486, 0.445600, -0.097014, 0.845266);
bone.local_frame_b = transform(0.000000, 0.000000, 0.000000, -0.201396, -0.001586, 0.901850, 0.382234);
bone.swing_limit = 60.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -5.0 * DEG_TO_RAD, y: 5.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.0, 0.0, 0.0),
center2: vec3(-0.091118, 0.037775, 0.229719),
radius: 0.075,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_UPPER_ARM_L].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_LOWER_ARM_L];
bone.parent_index = BONE_UPPER_ARM_L as i32;
body_def.name = "lower_arm_l".to_string();
bone.reference_frame = transform(0.305614, 1.242908, -0.117599, 0.165048, 0.563437, -0.802002, 0.109959);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Revolute;
bone.local_frame_a = transform(-0.095482, 0.039584, 0.240723, 0.512487, -0.180629, 0.839474, 0.003742);
bone.local_frame_b = transform(0.0, 0.0, 0.0, 0.503803, -0.029831, 0.858168, 0.094017);
bone.twist_limit = Vec2 { x: -5.0 * DEG_TO_RAD, y: 60.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.0, 0.0, 0.0),
center2: vec3(-0.142406, 0.039392, 0.261092),
radius: 0.05,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_LOWER_ARM_L].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_UPPER_ARM_R];
bone.parent_index = BONE_SPINE_03 as i32;
body_def.name = "upper_arm_r".to_string();
bone.reference_frame = transform(-0.20378, 1.484276, -0.115899, 0.143083, -0.695978, 0.690132, 0.137329);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Spherical;
bone.local_frame_a = transform(-0.203779, -0.069371, -0.181922, -0.253621, -0.414842, 0.106962, 0.867261);
bone.local_frame_b = transform(0.000000, 0.000000, 0.000000, -0.201397, 0.001587, -0.901850, 0.382233);
bone.swing_limit = 60.0 * DEG_TO_RAD;
bone.twist_limit = Vec2 { x: -5.0 * DEG_TO_RAD, y: 5.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.0, 0.0, 0.0),
center2: vec3(0.091118, 0.037775, 0.229718),
radius: 0.075,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_UPPER_ARM_R].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
{
let bone = &mut human.bones[BONE_LOWER_ARM_R];
bone.parent_index = BONE_UPPER_ARM_R as i32;
body_def.name = "lower_arm_r".to_string();
bone.reference_frame = transform(-0.305614, 1.242907, -0.117599, 0.165048, -0.563437, 0.802002, 0.109959);
body_def.rotation = bone.reference_frame.q;
body_def.position = offset_pos(position, bone.reference_frame.p);
bone.joint_type = BoneJointType::Revolute;
bone.local_frame_a = transform(0.095484, 0.039585, 0.240723, -0.180627, 0.512487, -0.003744, -0.839474);
bone.local_frame_b = transform(0.0, 0.0, 0.0, -0.029831, 0.503803, -0.094017, -0.858169);
bone.twist_limit = Vec2 { x: -60.0 * DEG_TO_RAD, y: 5.0 * DEG_TO_RAD };
let capsule = Capsule {
center1: vec3(0.0, 0.0, 0.0),
center2: vec3(0.142406, 0.039392, 0.261092),
radius: 0.05,
};
shape_def.filter.group_index = 0;
let body_id = create_body(world, &body_def);
human.bones[BONE_LOWER_ARM_R].body_id = body_id;
create_capsule_shape(world, body_id, &shape_def, &capsule);
}
// Create joints
for i in 1..BONE_COUNT {
let parent_index = human.bones[i].parent_index as usize;
let body_id_a = human.bones[parent_index].body_id;
let body_id_b = human.bones[i].body_id;
human.bones[i].local_frame_a.q = normalize_quat(human.bones[i].local_frame_a.q);
human.bones[i].local_frame_b.q = normalize_quat(human.bones[i].local_frame_b.q);
let bone = &human.bones[i];
match bone.joint_type {
BoneJointType::Revolute => {
let mut joint_def = default_revolute_joint_def();
joint_def.base.body_id_a = body_id_a;
joint_def.base.body_id_b = body_id_b;
joint_def.base.local_frame_a = bone.local_frame_a;
joint_def.base.local_frame_b = bone.local_frame_b;
joint_def.enable_limit = true;
joint_def.lower_angle = bone.twist_limit.x;
joint_def.upper_angle = bone.twist_limit.y;
joint_def.enable_spring = hertz > 0.0;
joint_def.hertz = hertz;
joint_def.damping_ratio = damping_ratio;
joint_def.enable_motor = true;
joint_def.max_motor_torque = bone.joint_friction * friction_torque;
let joint_id = create_revolute_joint(world, &joint_def);
human.bones[i].joint_id = joint_id;
}
BoneJointType::Spherical => {
let mut joint_def = default_spherical_joint_def();
joint_def.base.body_id_a = body_id_a;
joint_def.base.body_id_b = body_id_b;
joint_def.base.local_frame_a = bone.local_frame_a;
joint_def.base.local_frame_b = bone.local_frame_b;
joint_def.enable_cone_limit = true;
joint_def.cone_angle = bone.swing_limit;
joint_def.enable_twist_limit = true;
joint_def.lower_twist_angle = bone.twist_limit.x;
joint_def.upper_twist_angle = bone.twist_limit.y;
joint_def.enable_spring = hertz > 0.0;
joint_def.hertz = hertz;
joint_def.damping_ratio = damping_ratio;
joint_def.enable_motor = true;
joint_def.max_motor_torque = bone.joint_friction * friction_torque;
let joint_id = create_spherical_joint(world, &joint_def);
human.bones[i].joint_id = joint_id;
}
BoneJointType::None => {}
}
}
// Disable some collisions
let mut filter_def = default_filter_joint_def();
filter_def.base.body_id_a = human.bones[BONE_THIGH_L].body_id;
filter_def.base.body_id_b = human.bones[BONE_THIGH_R].body_id;
let filter_joint = create_filter_joint(world, &filter_def);
human.filter_joints.push(filter_joint);
human
}
// ---------------------------------------------------------------------------
// determinism.c scenario
// ---------------------------------------------------------------------------
const RAGDOLL_GROUP_SIZE: usize = 2;
const RAGDOLL_GRID_COUNT: usize = 2;
const GRID_SIZE: f32 = 15.0;
#[derive(Default)]
struct FallingRagdollData {
groups: Vec<Vec<Human>>, // [grid*grid][group_size]
step_count: i32,
sleep_step: i32,
hash: u32,
}
fn create_group(data: &mut FallingRagdollData, world: &mut World, row_index: usize, column_index: usize) {
let group_index = row_index * RAGDOLL_GRID_COUNT + column_index;
let span = RAGDOLL_GRID_COUNT as f32 * GRID_SIZE;
let group_distance = 1.0 * span / RAGDOLL_GRID_COUNT as f32;
let mut position = makepad_box3d::math_functions::pos(
-0.5 * span + group_distance * (column_index as f32 + 0.5),
15.0,
-0.5 * span + group_distance * (row_index as f32 + 0.5),
);
let friction_torque = 5.0;
let hertz = 1.0;
let damping_ratio = 0.7;
for _ in 0..RAGDOLL_GROUP_SIZE {
let human = create_human(world, position, friction_torque, hertz, damping_ratio, group_index as i32);
data.groups[group_index].push(human);
position.x += 0.75;
}
}
fn create_falling_ragdolls(world: &mut World) -> FallingRagdollData {
let mut data = FallingRagdollData::default();
for _ in 0..RAGDOLL_GRID_COUNT * RAGDOLL_GRID_COUNT {
data.groups.push(Vec::new());
}
data.sleep_step = 0;
let half_mesh_grid_rows = 4;
let mesh_grid_cell_width = GRID_SIZE / (2.0 * half_mesh_grid_rows as f32);
let grid_mesh = create_grid_mesh(2 * half_mesh_grid_rows, 2 * half_mesh_grid_rows, mesh_grid_cell_width, 0, true);
let torus_mesh = create_torus_mesh(16, 16, 0.25 * GRID_SIZE, 1.0);
let span = GRID_SIZE * RAGDOLL_GRID_COUNT as f32;
let mut body_def = default_body_def();
let shape_def = default_shape_def();
// C mutates position.x/.z in place; track f32 locals and assign through pos()
// so the float expressions stay identical in both precision modes.
let mut px = -0.5 * span + 0.5 * GRID_SIZE;
for i in 0..RAGDOLL_GRID_COUNT {
let mut pz = -0.5 * span + 0.5 * GRID_SIZE;
for j in 0..RAGDOLL_GRID_COUNT {
body_def.position = makepad_box3d::math_functions::pos(px, 0.0, pz);
let body = create_body(world, &body_def);
create_mesh_shape(world, body, &shape_def, &grid_mesh, Vec3::ONE);
create_mesh_shape(world, body, &shape_def, &torus_mesh, Vec3::ONE);
create_group(&mut data, world, i, j);
pz += GRID_SIZE;
}
px += GRID_SIZE;
}
data
}
#[cfg(not(feature = "double-precision"))]
fn hash_world_transform(h: u32, xf: &WorldTransform) -> u32 {
// C hashes the raw 28 bytes of b3WorldTransform (float mode): p then q.
let mut bytes = [0u8; 28];
bytes[0..4].copy_from_slice(&xf.p.x.to_le_bytes());
bytes[4..8].copy_from_slice(&xf.p.y.to_le_bytes());
bytes[8..12].copy_from_slice(&xf.p.z.to_le_bytes());
bytes[12..16].copy_from_slice(&xf.q.v.x.to_le_bytes());
bytes[16..20].copy_from_slice(&xf.q.v.y.to_le_bytes());
bytes[20..24].copy_from_slice(&xf.q.v.z.to_le_bytes());
bytes[24..28].copy_from_slice(&xf.q.s.to_le_bytes());
hash(h, &bytes)
}
#[cfg(feature = "double-precision")]
fn hash_world_transform(h: u32, xf: &WorldTransform) -> u32 {
// Double precision: the position is three f64 (C hashes the raw 40-byte struct).
let mut bytes = [0u8; 40];
bytes[0..8].copy_from_slice(&xf.p.x.to_le_bytes());
bytes[8..16].copy_from_slice(&xf.p.y.to_le_bytes());
bytes[16..24].copy_from_slice(&xf.p.z.to_le_bytes());
bytes[24..28].copy_from_slice(&xf.q.v.x.to_le_bytes());
bytes[28..32].copy_from_slice(&xf.q.v.y.to_le_bytes());
bytes[32..36].copy_from_slice(&xf.q.v.z.to_le_bytes());
bytes[36..40].copy_from_slice(&xf.q.s.to_le_bytes());
hash(h, &bytes)
}
fn update_falling_ragdolls(world: &World, data: &mut FallingRagdollData) -> bool {
if data.hash == 0 {
let move_count = world_get_body_events(world).move_events.len();
if move_count == 0 {
let awake_count = world_get_awake_body_count(world);
assert!(awake_count == 0, "no move events but {} awake bodies", awake_count);
data.hash = HASH_INIT;
for i in 0..RAGDOLL_GRID_COUNT {
for j in 0..RAGDOLL_GRID_COUNT {
for k in 0..RAGDOLL_GROUP_SIZE {
let group_index = i * RAGDOLL_GRID_COUNT + j;
let human = &data.groups[group_index][k];
for b in 0..BONE_COUNT {
let body_id = human.bones[b].body_id;
let xf = body_get_transform(world, body_id);
data.hash = hash_world_transform(data.hash, &xf);
}
}
}
}
data.sleep_step = data.step_count;
}
}
data.step_count += 1;
data.hash != 0
}
fn run_scenario() -> (i32, u32) {
run_scenario_with_workers(1)
}
fn run_scenario_with_workers(worker_count: u32) -> (i32, u32) {
let mut world_def = default_world_def();
world_def.worker_count = worker_count;
run_scenario_with_def(world_def)
}
fn run_scenario_with_def(world_def: makepad_box3d::types::WorldDef) -> (i32, u32) {
let mut world = create_world(&world_def);
let mut data = create_falling_ragdolls(&mut world);
let time_step = 1.0 / 60.0;
let step_limit = 1000;
let mut done = false;
for _ in 0..step_limit {
let sub_step_count = 4;
world_step(&mut world, time_step, sub_step_count);
done = update_falling_ragdolls(&world, &mut data);
if done {
break;
}
}
destroy_world(world);
assert!(done, "ragdolls did not settle within {} steps", step_limit);
(data.sleep_step, data.hash)
}
// C: test_determinism.c runs the scenario with 1..N workers and requires the
// same sleep step and hash for every worker count (thread-count determinism).
#[test]
fn determinism_across_worker_counts() {
let (sleep_step1, hash1) = run_scenario_with_workers(1);
println!("workers=1: sleepStep={} hash=0x{:08X}", sleep_step1, hash1);
for worker_count in [2u32, 4u32] {
let (sleep_step, hash) = run_scenario_with_workers(worker_count);
println!("workers={}: sleepStep={} hash=0x{:08X}", worker_count, sleep_step, hash);
ensure!(sleep_step == sleep_step1);
ensure!(hash == hash1);
}
ensure!(hash1 != 0);
}
// PORT EXTENSION — not in upstream C. The adaptive broad-phase hybrid
// (WorldDef.enable_broad_phase_hybrid) re-baselines the hash (contact creation
// order changes), but must still be fully deterministic: the parallel BVTT
// batch is canonically sorted, so its result is independent of worker count
// and work distribution. This asserts the ON-path hash is identical across
// worker counts (and, in debug builds, exercises the batch-vs-per-mover
// SET-equality assertion inside update_broad_phase_pairs at every count).
#[test]
fn determinism_broad_phase_hybrid_across_worker_counts() {
let mut def1 = default_world_def();
def1.worker_count = 1;
def1.enable_broad_phase_hybrid = true;
let (sleep_step1, hash1) = run_scenario_with_def(def1);
println!("hybrid workers=1: sleepStep={} hash=0x{:08X}", sleep_step1, hash1);
for worker_count in [2u32, 4u32] {
let mut def = default_world_def();
def.worker_count = worker_count;
def.enable_broad_phase_hybrid = true;
let (sleep_step, hash) = run_scenario_with_def(def);
println!("hybrid workers={}: sleepStep={} hash=0x{:08X}", worker_count, sleep_step, hash);
ensure!(sleep_step == sleep_step1);
ensure!(hash == hash1);
}
ensure!(hash1 != 0);
}
// The external task-system hooks (C: b3WorldDef enqueueTask/finishTask) must
// also be bit-deterministic: the same tasks run, just on user threads.
#[test]
fn determinism_with_external_task_system() {
let (sleep_step1, hash1) = run_scenario_with_workers(1);
let mut world_def = default_world_def();
world_def.worker_count = 4;
world_def.enqueue_task = Some(makepad_box3d::test_utils::thread_per_task_enqueue);
world_def.finish_task = Some(makepad_box3d::test_utils::thread_per_task_finish);
let (sleep_step, hash) = run_scenario_with_def(world_def);
println!("external tasks: sleepStep={} hash=0x{:08X}", sleep_step, hash);
ensure!(sleep_step == sleep_step1);
ensure!(hash == hash1);
ensure!(hash1 != 0);
}
#[test]
fn determinism_test() {
let (sleep_step1, hash1) = run_scenario();
println!("run 1: sleepStep={} hash=0x{:08X}", sleep_step1, hash1);
let (sleep_step2, hash2) = run_scenario();
println!("run 2: sleepStep={} hash=0x{:08X}", sleep_step2, hash2);
ensure!(sleep_step1 == sleep_step2);
ensure!(hash1 == hash2);
ensure!(hash1 != 0);
}