macrosoft3dpinball/godot/BOX3D_ENGINE_NOTES.md
m3ultra 5fa6bbf67b Add Box3D upstream research notes; note godot-box3d is single-threaded
Engine notes from the eval session (Apple Silicon 24-P-core worker cap, the
benchmark harness, binding gaps, provenance), linked from the README.
Annotated with a verified fact: the binding hardcodes workerCount=1, so the
E-core trap cannot trigger through Godot until it grows a task system.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:03:34 +10:00

7.1 KiB
Raw Permalink Blame History

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×).

Status in godot-box3d (verified 2026-08-06 against the commit vendored in bin/): the binding hardcodes def.workerCount = 1 in src/spaces/box3d_space_3d.cpp and wires no task system, so Godot runs Box3D single-threaded — there is no worker setting to cap, and the E-core trap can't trigger through Godot today. This section applies to the raw benchmark harness below, and becomes load-bearing the day the binding grows threading.

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 PinJoint3D anchor after creation — recreate the joint instead

And one behavioural divergence that will bite silently rather than error:

Area3D does not detect trimesh or heightmap bodies. In Box3D a concave or heightmap shape can never be a sensor visitor, by design. No body_entered / body_exited fires 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.