# 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 /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).