makepad/libs/box3d/PORTING.md
Admin 364d65680a box3d: performance pass — serial geomean 1.30x -> 1.13x vs C, 8-worker 1.6x -> 1.47x
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>
2026-07-05 09:10:44 +02:00

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_PRECISION off): Pos = Vec3, WorldTransform = Transform (type aliases).
  • SIMD: the B3_SIMD_NONE path only. b3V32 = [f32; 4] scalar emulation, B3_SIMD_WIDTH = 4.
  • Threading: parallel_for runs 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: b3Vec3Vec3, b3WorldDefWorldDef, b3AABBAABB.
    • Functions: snake_case: b3MakeQuatFromAxisAnglemake_quat_from_axis_angle, b3Body_GetPositionbody_get_position, b3World_Stepworld_step, b3DynamicTree_CreateProxydynamic_tree_create_proxy.
    • Constants: B3_LINEAR_SLOPlinear_slop() (function, depends on length units), B3_MAX_MANIFOLD_POINTSMAX_MANIFOLD_POINTS.
    • Enum values: b3_dynamicBodyBodyType::Dynamic, b3_hullShapeShapeType::Hull.
  • Struct fields: snake_case of the C name: lowerBoundlower_bound.
  • One Rust module per C file: src/dynamic_tree.rssrc/dynamic_tree.c. .h-only inline functions go in the module of the matching .c, or math_internal.rs etc. 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 + c shapes in hot paths are contracted with f32::mul_add (f64::mul_add for the double-precision twins) — the port's equivalent of clang's default -ffp-contract=on on the C build. Canonical patterns: a*b + ca.mul_add(b, c); a*b + c*da.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*z2z1.mul_add(z2, y1.mul_add(y2, x1*x2)). mul_add is 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 deterministic atan2/compute_cos_sin, or any hashing/serialization code.
  • b3Atan2 / b3ComputeCosSin are hand-written approximations — port digit for digit.
  • remainderf has no stable Rust equivalent; unwind_angle implements IEEE remainder via f64 (see math_functions.rs).
  • Use sqrtff32::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).