Profiling-driven (sample + disasm comparison vs clang -O3). All changes safe Rust except one debug_assert-guarded extension of the existing SyncSlice unsafe contract. Determinism preserved: hash bit-identical across workers 1/2/4, NEON/SSE2/scalar, internal/external task systems (new baseline 0x9018E2D8 after approved FMA contraction). - f32/f64::mul_add contraction in hot scalar math (= C's -ffp-contract=on; 89 sites; wide SIMD ops untouched like C intrinsics). large_pyramid now at parity with C (1387 vs 1373 ms serial) - FloatW::get/set: direct lane load/store instead of vector-through-stack round trip; layout asserted at compile time - gather_bodies by reference (removes 20-register spill storm) - per-worker capacity-preserving scratch for convex + mesh collide paths (C-arena equivalent; mesh path allocated per triangle and serialized the parallel collide pass on allocator locks) - update_contact: borrow shapes instead of cloning (deep compound geometry clones + cross-worker Arc traffic; junkyard w=8 -39%) - scheduler semaphore: two-level atomic fast path (C uses dispatch_semaphore_t; old Mutex+Condvar locked every enqueue) - SyncSlice::get_ref/get_mut unchecked indexing under the existing unsafe contract, debug_assert-guarded (-6% serial) - README/PORTING: new numbers, FMA sync conventions, known remainders 179/185/179 tests green (default/double-precision/disable-simd), zero warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.9 KiB
Box3D → Rust Porting Conventions
Rust port of Box3D (C17, by Erin Catto) from <repo>/box3d/. The port stays as
close to the C source as possible: same file structure, same function names,
same algorithm structure, same order of operations (float determinism!).
Do not "improve" algorithms or restructure logic. Translate mechanically.
Scope decisions
- Single precision only (
BOX3D_DOUBLE_PRECISIONoff):Pos = Vec3,WorldTransform = Transform(type aliases). - SIMD: the
B3_SIMD_NONEpath only.b3V32=[f32; 4]scalar emulation,B3_SIMD_WIDTH = 4. - Threading:
parallel_forruns serially (single worker). No scheduler threads. Keep the same code structure so stage ordering matches C with workerCount = 1. - Recording/replay/world_snapshot: not ported (yet).
- Tracy/dump/timer profiling: stubbed or
std::time. - No external crates. std only.
Naming
- Strip the
b3/B3_prefix, then:- Types: keep CamelCase:
b3Vec3→Vec3,b3WorldDef→WorldDef,b3AABB→AABB. - Functions: snake_case:
b3MakeQuatFromAxisAngle→make_quat_from_axis_angle,b3Body_GetPosition→body_get_position,b3World_Step→world_step,b3DynamicTree_CreateProxy→dynamic_tree_create_proxy. - Constants:
B3_LINEAR_SLOP→linear_slop()(function, depends on length units),B3_MAX_MANIFOLD_POINTS→MAX_MANIFOLD_POINTS. - Enum values:
b3_dynamicBody→BodyType::Dynamic,b3_hullShape→ShapeType::Hull.
- Types: keep CamelCase:
- Struct fields: snake_case of the C name:
lowerBound→lower_bound. - One Rust module per C file:
src/dynamic_tree.rs←src/dynamic_tree.c..h-only inline functions go in the module of the matching.c, ormath_internal.rsetc. for header-only files.
Core mappings
| C | Rust |
|---|---|
b3Vec3 struct literal (b3Vec3){x,y,z} |
vec3(x, y, z) helper |
B3_NULL_INDEX (-1) |
NULL_INDEX (i32 = -1) |
int indices |
i32, cast as usize at slice access |
b3Array(T) dynamic arrays |
Vec<T> |
out-params float* t |
&mut f32 (keep out-param style) |
void* userData |
u64 (default 0) |
const char* name in defs |
String |
| B3_ASSERT | b3_assert! (→ debug_assert!) |
| B3_VALIDATE | b3_validate! (→ debug_assert!) |
b3Alloc/b3Free |
not needed; use Vec/Box |
arena allocator (b3AllocateArenaItem) |
Vec<T> scratch allocations |
verstable hash sets/maps (table.h) |
std::collections wrapper in table.rs |
const b3HullData* (user-owned) |
Arc<HullData> |
const b3MeshData* |
Arc<MeshData> |
| heightfield/compound data | Arc<...> |
| structs with trailing variable data + offsets | plain struct with Vec fields |
function-pointer + void* context query callbacks |
&mut dyn FnMut(...) |
| stored callbacks (friction mix etc.) | Option<fn(...)> |
| two mutable elements of one array | get_two_mut(&mut vec, i, j) from core.rs, or copy-out/copy-in for small PODs |
Public API
The C public API is handle-based with a hidden global world array
(b3Body_GetPosition(bodyId)). In the port, World is an owned struct and the
world parameter is explicit; everything else keeps the C name:
let mut world = create_world(&world_def()); // b3CreateWorld
let body_id = create_body(&mut world, &body_def); // b3CreateBody
let p = body_get_position(&world, body_id); // b3Body_GetPosition
Ids (BodyId, ShapeId, JointId, ContactId) keep their C layout
(index1/world0/generation) and null convention (index1 == 0 is null,
so stored index is index + 1).
Float determinism
- Never reorder float expressions; keep temporaries and parenthesization.
- Scalar
a*b + cshapes in hot paths are contracted withf32::mul_add(f64::mul_addfor the double-precision twins) — the port's equivalent of clang's default-ffp-contract=onon the C build. Canonical patterns:a*b + c→a.mul_add(b, c);a*b + c*d→a.mul_add(b, c*d)(one fma per add); sum chains fuse left-to-right with the FIRST product plain:x1*x2 + y1*y2 + z1*z2→z1.mul_add(z2, y1.mul_add(y2, x1*x2)).mul_addis IEEE correctly rounded on every target, so this preserves all determinism properties. Do NOT contract: the wide NEON/SSE2 ops in contact_solver.rs (C's intrinsics aren't contracted either), the deterministicatan2/compute_cos_sin, or any hashing/serialization code. b3Atan2/b3ComputeCosSinare hand-written approximations — port digit for digit.remainderfhas no stable Rust equivalent;unwind_angleimplements IEEE remainder via f64 (see math_functions.rs).- Use
sqrtf→f32::sqrt(IEEE, deterministic). Do not use other libm functions (sin/cos/atan2 from std) inside the engine.
Layout of a ported module
Keep the C file's function order. Put a header comment naming the source file.
Static file-local helpers stay fn (not pub). Everything referenced from
other modules is pub(crate) or pub (public API).