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>
99 lines
4.2 KiB
Markdown
99 lines
4.2 KiB
Markdown
# Godot physics testbed (Box3D / Jolt / GodotPhysics A/B)
|
||
|
||
A minimal playable pinball table for Godot 4.7 whose real job is comparing 3D physics
|
||
backends on the things pinball stresses: fast-small-ball CCD, motorised hinge flippers,
|
||
and joint limits. The backend is one project setting, so the same table runs under all
|
||
three engines.
|
||
|
||
## Run it
|
||
|
||
```bash
|
||
/Applications/Godot.app/Contents/MacOS/Godot --path godot
|
||
```
|
||
|
||
Keys: `Z` left flipper, `/` right flipper, `Space` plunger, `R` reset ball.
|
||
|
||
The project defaults to `Box3D Physics (Extension)` via
|
||
[godot-box3d](https://github.com/bearlikelion/godot-box3d) (MIT). `bin/` carries a
|
||
macOS arm64 dylib built from source (upstream has no releases; build recipe below).
|
||
|
||
## A/B probe harness
|
||
|
||
```bash
|
||
godot/run_probe.sh # GODOT_BIN=... to point at a different binary
|
||
```
|
||
|
||
Writes `override.cfg` to switch `physics/3d/physics_engine`, runs `probe/probe.tscn`
|
||
headless under each backend, and prints per-engine results (also saved to
|
||
`probe/results/`, untracked). Two tests:
|
||
|
||
1. **Tunneling** — ball fired at a 1 cm static wall at 5–160 m/s, CCD off and on.
|
||
2. **Hinge motor** — a motorised HingeJoint3D arm; a flipper is a motorised hinge.
|
||
|
||
There is also a table-level flipper check:
|
||
`PINBALL_AUTOTEST=1 godot --headless --path godot` flips both real flippers for 0.5 s
|
||
and prints the swing angles.
|
||
|
||
## Results (2026-08-06, Godot 4.7.stable, M3 Ultra)
|
||
|
||
Tunneling — ball contained by the 1 cm wall?
|
||
|
||
| speed m/s | Godot ccd off | Jolt ccd off | Box3D ccd off | all three, ccd on |
|
||
|---|---|---|---|---|
|
||
| 5 | tunneled* | tunneled* | **contained** | contained |
|
||
| 10 | tunneled | contained* | **contained** | contained |
|
||
| 20 | contained* | contained* | **contained** | contained |
|
||
| 40 | contained* | contained* | **contained** | contained |
|
||
| 80 | tunneled | tunneled | **contained** | contained |
|
||
| 160 | tunneled | tunneled | **contained** | contained |
|
||
|
||
\* With a 60 Hz tick, every speed here can step over a 1 cm wall; the discrete-engine
|
||
"contained" rows are phase luck (2 m at 20 and 40 m/s lands a tick exactly on the wall
|
||
— and both engines produce identical trajectories, so they get lucky identically).
|
||
The signal is the pattern, not individual rows: **Box3D never tunnels even with the
|
||
CCD flag off** (speculative contacts are always on), the other two need
|
||
`continuous_cd = true`, and with it all three hold at 160 m/s.
|
||
|
||
Hinge motor (target 5 rad/s, 1 s):
|
||
|
||
| engine | reached rad/s | moved | verdict |
|
||
|---|---|---|---|
|
||
| GodotPhysics3D | −5.0 | 73.5° (wrapped) | works |
|
||
| Jolt | −5.0 | 75.1° (wrapped) | works |
|
||
| Box3D | **+5.0** | 73.5° (wrapped) | works |
|
||
|
||
Flipper autotest on the real table (motors at ±25 rad/s, limits −5°…+50°):
|
||
|
||
| engine | left swing | right swing |
|
||
|---|---|---|
|
||
| Box3D | +51.8° — **stops at the 50° limit** | −51.8° — stops at limit |
|
||
| Jolt | −106.6° — wrong way, **blows ~100° past the limit** | +104.3° — same |
|
||
|
||
Two real findings, half a second of headless runtime each:
|
||
|
||
1. **Box3D's hinge motor sign is inverted** relative to GodotPhysics/Jolt (upstream
|
||
drop-in-compatibility bug candidate for godot-box3d). The table's flipper signs are
|
||
tuned for Box3D, the project default — flip `FLIP_SPEED`/`REST_SPEED` signs in
|
||
`scenes/table.gd` to play under Jolt.
|
||
2. With the motor overdriving into them, **Box3D enforces the angular limits hard**
|
||
while Jolt lets this setup punch far through them.
|
||
|
||
## Engine notes
|
||
|
||
Upstream Box3D research — Apple Silicon threading (**cap workers at 24, not 32**), the benchmark
|
||
harness and how to re-run it, what godot-box3d does *not* support, and why this game became the
|
||
testbed — is in [BOX3D_ENGINE_NOTES.md](BOX3D_ENGINE_NOTES.md).
|
||
|
||
## Rebuilding the extension
|
||
|
||
```bash
|
||
git clone --recurse-submodules https://github.com/bearlikelion/godot-box3d
|
||
cd godot-box3d && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
|
||
cp bin/libgodot-box3d.dylib <here>/bin/
|
||
```
|
||
|
||
Known Box3D gaps that don't matter for pinball: no Generic6DOF/ConeTwist joints, no
|
||
SoftBody3D, Area3D can't detect trimesh/heightmap bodies (the ball is a sphere; fine).
|
||
Box3D also ignores Godot's hinge BIAS/LIMIT_SOFTNESS/LIMIT_RELAXATION params (warns at
|
||
load; harmless here).
|