Findings from the engine evaluation that led to this project, kept separate from the table's own A/B results: - Apple Silicon threading: Box3D gets SLOWER past the performance-core count on the M3 Ultra (30 workers 5158ms -> 32 workers 5646ms, 24P+8E). A physics step is a barrier, so a share of the work on a third-speed E-core makes the whole step wait. Read hw.perflevel0.physicalcpu, never hw.ncpu. Applies to any threaded physics on this fleet, not just Box3D. - The benchmark harness: the CMake option is BOX3D_BENCHMARKS (plural), the scene/flag list, upstream's M2 Air + AMD baselines, and a warning that a full sweep runs over an hour on 32 cores — always pin -b and -w. - What the binding does NOT implement (Generic6DOF, ConeTwist, SoftBody3D, per-pair exceptions) plus the silent Area3D-vs-trimesh divergence. - Why pinball and not the ragdoll game: ~/Documents/alright is the better fit but is BLOCKED — its ragdoll is built from Generic6DOFJoint3D. Watch that upstream. - Provenance: Box3D is serious (Catto, MIT, s&box + Esoterica adoption); the binding is five weeks old with zero releases and self-declares experimental. This is a testbed, not a bet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.6 KiB
Box3D — upstream research notes
Findings from the evaluation session that led to this testbed existing (2026-08-06, M3 Ultra). Complements README.md, which covers this project's own A/B probe results. Nothing here is about the table — it's about the engine underneath it and the hardware it runs on.
1. Cap physics workers at 24 on this machine, not 32
The single most actionable finding, and it applies to any threaded physics on this fleet — Box3D, Jolt, or anything else.
The M3 Ultra is 24 performance + 8 efficiency cores. Box3D's own benchmark, swept across worker counts on this machine, gets slower once work spills onto the E-cores:
| workers | ms | |
|---|---|---|
| 30 | 5,158 | |
| 31 | 5,310 | |
| 32 | 5,646 | 10% worse than 30 |
A physics step is a barrier — every worker waits for the slowest one — so handing a share of the work to a core that runs at roughly a third the speed makes the whole step wait on it. More threads is not more throughput past the performance-core count.
So: don't set worker count from OS.get_processor_count() / hw.ncpu on Apple Silicon.
Read the performance-core count instead:
sysctl -n hw.perflevel0.physicalcpu # 24 on this box — use this
sysctl -n hw.ncpu # 32 — do NOT use this
Within the performance cores the threading scales well: upstream's M2 Air joint_grid figures
are 1,089 ms single-threaded → 309 ms on 4 threads (3.52×).
Caveat on the table above: it is one scene (8,002 bodies / 40,444 contacts), best-of-4 runs, and the low-thread-count rows of that particular sweep were lost to a truncated capture. The direction is unambiguous and reproduces; treat the exact millisecond figures as indicative. A pinball table is ~1 ball and a few dozen bodies, so none of this is a bottleneck here — it matters when this testbed is used to answer questions for the bigger physics games.
2. The benchmark harness, and how to re-run it
Box3D ships a real benchmark suite. It is not built by default — note the option is
BOX3D_BENCHMARKS, plural (an easy 20 minutes to lose):
git clone --depth 1 https://github.com/erincatto/box3d && cd box3d
cmake --preset macos -DBOX3D_BENCHMARKS=ON
cmake --build --preset macos-release --target benchmark -j 24
./build/bin/Release/benchmark --list
Flags: -b <index|name> one scene · -w <n> a single worker count · -t <n> max threads ·
-r <n> repeats (default 4) · -nc disable continuous collision · -s record step times.
Scenes: convex_pile, joint_grid, junkyard, large_pyramid, large_world, many_pyramids,
rain, trees100/50/25, washer. Upstream keeps reference results in benchmark/m2air_neon/
(Apple Silicon) and benchmark/amd7950x_* — useful baselines to compare a run against.
Budget the time. A full sweep is ~11 scenes × every worker count × 4 repeats and runs for
well over an hour on 32 cores. Always pin -b and -w, and capture with tee, not tail.
The library itself builds clean on Apple Silicon (cmake --preset macos) and its whole test
suite passes in 0.61 s — a fast, honest smoke test after any upstream bump.
3. What godot-box3d does NOT support
From the binding's own README. Hitting one of these looks like an engine bug and isn't:
- Generic6DOF and ConeTwist joints — not implemented
SoftBody3D— not implemented- Separation ray shapes
- Per-pair collision exceptions (use layers/masks)
- Changing a
PinJoint3Danchor after creation — recreate the joint instead
And one behavioural divergence that will bite silently rather than error:
Area3Ddoes not detect trimesh or heightmap bodies. In Box3D a concave or heightmap shape can never be a sensor visitor, by design. Nobody_entered/body_exitedfires for such a body. Godot's built-in physics and Jolt both report it. Give anything that must be area-detected a convex shape.
Pinball is unaffected — it needs hinge joints (supported) and CCD (Box3D's strength), and none of the above. That is exactly why this became the testbed game.
4. Why pinball, and not the ragdoll game
~/Documents/alright (the fall-down-the-stairs game) is the portfolio's strongest fit for
Box3D — an active ragdoll of 14 rigid bodies and 12 joints with PD motor control, already paying
physics_ticks_per_second=120 and solver_iterations=16 and hand-picking continuous_cd on
head/torso/pelvis, which is the signature of fighting a solver.
It is blocked: that ragdoll is built from Generic6DOFJoint3D, which the binding does not
implement. Watch that single line item upstream — when Generic6DOF lands, alright becomes the
real experiment, and the question to answer is whether the 120 Hz / 16-iteration tax can come
back down.
Elsewhere in the portfolio physics is 2D, absent, or deliberately bespoke (Shitbox's car is a
raycast-suspension RigidBody3D with a hand-written tyre model — its feel lives in its code,
not the solver; Destroyulator's cascade is a support graph). toastsim is the only browser game
with a real 3D solver need and it already runs Rapier.
5. Provenance and maturity — worth being clear-eyed about
Box3D — Erin Catto (author of Box2D), MIT, portable C17, data-oriented, ARM NEON SIMD, cross-platform determinism, record/replay. Repo created 2026-05-10, announced 2026-06-30, ~5,900 stars, actively developed. Shipping adoption: Facepunch's s&box and the Esoterica engine. This is a serious engine by someone with 20+ years of solver experience.
godot-box3d (bearlikelion) — created 2026-07-02, ~134 stars, zero releases, and its README says plainly: "Status: early and experimental." Performance benchmarking is still on its own to-do list, so no one has yet shown it beats Jolt at anything. Modelled on godot-jolt's approach. There are no prebuilt binaries — build from source (~30 s on this machine).
Practical consequence: this project is a testbed, not a bet. Jolt remains the sane default for anything shipping; the value here is being able to answer engine questions with measurements instead of opinions.
6. Browser note
Box3D builds to WASM — its CMake maps the SSE2 path onto wasm SIMD128 and supports pthreads. But it ships no JS bindings, so a browser game would mean hand-writing that layer. Rapier already gives you a polished JS API. Relevant if a web pinball or web physics toy is ever floated.
Cross-reference: this project's own measured results — Box3D never tunnelling even with CCD off, and the inverted hinge-motor sign versus Jolt/GodotPhysics — are in README.md. That sign inversion is a genuine drop-in-compatibility bug worth reporting upstream.