clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

M9: physics spike - Box3D confirmed (WASM, deterministic, 3800x realtime)

Dami committed bab478b
Box3D (Erin Catto, June 2026, MIT, C17) builds to WASM via emcmake with
zero patches. Spike: marble-sized sphere slaloming down a tilted trough -
bit-identical determinism across runs (advertised cross-platform
determinism + record/replay), ~228k steps/s single-worker (~3800x
realtime at 60Hz x 4 substeps), contact precision exact to the geometric
wall plane, double-precision positions (fits unbounded endless coords).
Spike source + full gate results in tools/physics-spike/.
commit
bab478bbedf561bc182204bf486494cb9205fdc7
parent
bcae1f2fc6dde9881007fe0f00843631c869bf3c
author
Dami <dami.etoile@gmail.com>
2 files changed, +142 -0
Atools/physics-spike/README.md47+++++++++++++++
Atools/physics-spike/spike.c95++++++++++++++++++++++++++++++
A tools/physics-spike/README.md +47 -0
@@ -0,0 +1,47 @@
1+# M9 physics spike — Box3D → WASM (2026-08-08)2+3+**Verdict: Box3D confirmed as the physics-era engine.**4+5+Box3D (Erin Catto, released 2026-06-30, MIT, C17) — the 3D successor to6+Box2D. Spiked at commit `3fc20f5b453ba9e14cdf54ecafa87a2a4bcdf53c`.7+8+## Spike setup9+10+- `git clone --depth 1 https://github.com/erincatto/box3d && cd box3d`11+- `emcmake cmake -B build-wasm -DCMAKE_BUILD_TYPE=Release -DBOX3D_SAMPLES=OFF12+ -DBOX3D_UNIT_TESTS=OFF -DBOX3D_BENCHMARKS=OFF && cmake --build build-wasm`13+ — builds `libbox3d.a` for wasm32 with ZERO patches.14+- `emcc spike.c libbox3d.a -I include -O2 -o spike.js && node spike.js`15+16+`spike.c` here: a slalom-like trough (floor + walls + end caps, tilted 5°)17+with a marble-sized (r=0.2) sphere rolling down 60 units, 1200 steps at18+60 Hz × 4 substeps.19+20+## Gate results21+22+- **Determinism**: position-trace FNV hash bit-identical, both in-process23+ (two worlds) and across separate runs. Box3D advertises cross-platform24+ determinism + record/replay — matches what we measured.25+- **Performance**: ~228k steps/s single-worker under Node (≈3,800×26+ realtime at 60 Hz; the gate was ≥4×). Effectively unlimited headroom.27+- **Behavior**: marble slalom maxAbsX settled at exactly the geometric28+ wall-contact plane (0.75 − 0.05 wall − 0.2 radius = 0.500) — contact29+ precision is excellent. Escaped-marble freefall behaves as expected30+ (first run tumbled into the void forever — endless-mode-compatible).31+- **API**: portable C API in the Box2D v3 style (b3DefaultWorldDef /32+ b3CreateBody / b3CreateHullShape / b3CreateSphereShape /33+ b3CreateMeshShape / b3World_Step / b3Body_GetPosition). Double-precision34+ positions by default (b3CreateWorld = b3CreateWorldDoublePrecision) —35+ good for the endless line's unbounded coordinates.36+- **Size**: spike .wasm 369 KB (-O2, whole library linked).37+38+## M10 integration plan39+40+- Vendor box3d pinned at the commit above (or a submodule) + a build task41+ producing `static/dist/box3d.js/.wasm`.42+- Hand-written C shim exporting exactly the calls the marble needs43+ (world/step/static hulls+meshes per piece proxy, one dynamic sphere,44+ transforms out) — keep the JS surface tiny.45+- Collision proxies per PieceDef measured from the GLBs (floor ribbon +46+ wall strips as hulls; helix/curved floors as trimesh), authored in the47+ registry like centerlines.
A tools/physics-spike/spike.c +95 -0
@@ -0,0 +1,95 @@
1+// M9 spike: Box3D → WASM viability for the marbles physics era.2+//3+// A slalom-like trough (floor + two walls, tilted 5°) with a marble-sized4+// sphere rolling down it. Checks, in order:5+// 1. It simulates sanely (ball stays in the channel, rolls downhill).6+// 2. Determinism: two full sims in-process hash identically; the host7+// runs the binary twice to confirm run-to-run identity.8+// 3. Performance: steps/second at 60 Hz × 4 substeps, single worker.9+#include "box3d/box3d.h"10+#include <stdio.h>11+#include <stdint.h>12+#include <string.h>13+#include <time.h>14+15+static uint64_t hash64(uint64_t h, const void* data, size_t n) {16+ const uint8_t* p = (const uint8_t*)data;17+ for (size_t i = 0; i < n; i++) {18+ h ^= p[i];19+ h *= 1099511628211ULL; // FNV-1a20+ }21+ return h;22+}23+24+// One full sim: returns a position-trace hash; writes the final position.25+static uint64_t simulate(b3Pos* finalPos, double* maxAbsX) {26+ b3WorldDef wdef = b3DefaultWorldDef();27+ wdef.workerCount = 1;28+ b3WorldId world = b3CreateWorld(&wdef);29+30+ // Slalom trough: floor hx=0.7 (slalom half-width), walls at x=±0.75,31+ // 60 long, tilted 5° about x so the ball rolls toward +z... the tilt is32+ // about the X axis: rotate the trough body itself.33+ b3BodyDef ground = b3DefaultBodyDef();34+ ground.type = b3_staticBody;35+ ground.rotation = b3MakeQuatFromAxisAngle((b3Vec3){1, 0, 0}, 0.0873f);36+ b3BodyId groundId = b3CreateBody(world, &ground);37+ b3ShapeDef sdef = b3DefaultShapeDef();38+ b3BoxHull floor = b3MakeOffsetBoxHull(0.7f, 0.1f, 30.0f, (b3Vec3){0, -0.1f, 0});39+ b3CreateHullShape(groundId, &sdef, &floor.base);40+ b3BoxHull wallL = b3MakeOffsetBoxHull(0.05f, 0.5f, 30.0f, (b3Vec3){-0.75f, 0.4f, 0});41+ b3CreateHullShape(groundId, &sdef, &wallL.base);42+ b3BoxHull wallR = b3MakeOffsetBoxHull(0.05f, 0.5f, 30.0f, (b3Vec3){0.75f, 0.4f, 0});43+ b3CreateHullShape(groundId, &sdef, &wallR.base);44+ b3BoxHull capA = b3MakeOffsetBoxHull(0.8f, 0.5f, 0.05f, (b3Vec3){0, 0.4f, -30.05f});45+ b3CreateHullShape(groundId, &sdef, &capA.base);46+ b3BoxHull capB = b3MakeOffsetBoxHull(0.8f, 0.5f, 0.05f, (b3Vec3){0, 0.4f, 30.05f});47+ b3CreateHullShape(groundId, &sdef, &capB.base);48+49+ // The marble: r=0.2 dynamic sphere near the high end, nudged off-center50+ // so it slaloms between the walls.51+ b3BodyDef ball = b3DefaultBodyDef();52+ ball.type = b3_dynamicBody;53+ ball.position = (b3Pos){0.2, 2.4, -24.0}; // on the tilted high end54+ ball.linearVelocity = (b3Vec3){0.6f, 0, 1.5f};55+ b3BodyId ballId = b3CreateBody(world, &ball);56+ b3Sphere sphere = {{0, 0, 0}, 0.2f};57+ b3ShapeDef ballShape = b3DefaultShapeDef();58+ b3CreateSphereShape(ballId, &ballShape, &sphere);59+60+ uint64_t h = 1469598103934665603ULL;61+ *maxAbsX = 0;62+ for (int i = 0; i < 1200; i++) { // 20 s at 60 Hz63+ b3World_Step(world, 1.0f / 60.0f, 4);64+ if (i % 30 == 0) {65+ b3Pos p = b3Body_GetPosition(ballId);66+ h = hash64(h, &p, sizeof p);67+ if (p.x > *maxAbsX) *maxAbsX = p.x;68+ if (-p.x > *maxAbsX) *maxAbsX = -p.x;69+ }70+ }71+ *finalPos = b3Body_GetPosition(ballId);72+ b3DestroyWorld(world);73+ return h;74+}75+76+int main(void) {77+ b3Pos a, b;78+ double maxX;79+ uint64_t h1 = simulate(&a, &maxX);80+ uint64_t h2 = simulate(&b, &maxX);81+ printf("hash1=%llx hash2=%llx in-process-deterministic=%s\n",82+ (unsigned long long)h1, (unsigned long long)h2,83+ h1 == h2 ? "YES" : "NO");84+ printf("final=(%.6f, %.6f, %.6f) maxAbsX=%.3f\n", a.x, a.y, a.z, maxX);85+86+ // Perf: fresh world, 6000 steps timed.87+ clock_t t0 = clock();88+ b3Pos dummy;89+ double dx;90+ for (int r = 0; r < 5; r++) simulate(&dummy, &dx);91+ double secs = (double)(clock() - t0) / CLOCKS_PER_SEC;92+ printf("perf: %d steps in %.3f s → %.0f steps/s (%.1fx realtime at 60 Hz)\n",93+ 5 * 1200, secs, 5 * 1200 / secs, (5 * 1200 / secs) / 60.0);94+ return 0;95+}