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

137 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Box3D — upstream research notes
*Findings from the evaluation session that led to this testbed existing (2026-08-06, M3 Ultra).
Complements [README.md](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:
```sh
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):
```sh
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](README.md). That
sign inversion is a genuine drop-in-compatibility bug worth reporting upstream.*