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>
137 lines
7.1 KiB
Markdown
137 lines
7.1 KiB
Markdown
# 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.*
|