Skip to content

Fixed3D v1.1.0

Choose a tag to compare

@gafferongames gafferongames released this 14 Jul 22:44

Fixed3D 1.1.0 is the first maintenance release. v1.0.0 answered the question this repository exists for; v1.1.0 hardens the answer: two integer-overflow energy-injection fixes in the solver's impulse caps, a structurally enforced 100 km scene origin everywhere, the first full per-sample visual verification against float Box3D, and a conversion-audit tool that now runs in CI.

You should probably still keep using vanilla Box3D. The transferable findings from this release joined the others in ERIN.md.

Fixed

  • Impulse-cap sentinel wrap (joints). Joint force/torque limits default to a B3_FIXED_MAX sentinel (the float FLT_MAX analog), and the caps compare impulse² > cap² — but (h·B3_FIXED_MAX)² wraps in int64, at h = 1/240 to exactly 0, so the "clamp" fired on the first nonzero impulse and rescaled it up to ~5.8×10¹¹. One misfire injected ~10¹⁰ rad/s into the Driving sample's chassis and stalled continuous collision for seconds per frame. The caps now compare at 128 bits, mirroring each site's exact rounding — bit-identical for all in-range values. Regression test included.
  • Impulse-cap wrap in the contact solver (friction/rolling). Same overflow class on physical impulses: the friction cone clamp squares μ·(per-iteration manifold normal sum), and legal heavy/high-friction content can push that product past the int64 wrap point — where the wrapped square goes negative, the clamp fires on every compare, and friction impulses get rescaled upward (energy injection). All six squared-cap sites now gate their operands (below the gate the historical code runs verbatim — bit-identical for all existing content) with a 128-bit cold path above it. Cost: +0.5–1% on the worst-case benchmark, accepted as the price of correctness. Regression test included.
  • The samples GUI never actually simulated. dt reached b3World_Step through a float→fixed truncation that made every interactive step dt = 0 — which had been hiding the joint cap bug above. Fixing it surfaced the rest of the iceberg: the samples renderer/host layer had ~1,850 silent raw-fixed-through-float conversions (cameras breaking beyond 32 km, seven samples crashing on degenerate hulls, broken IBL/GTAO, a non-functional character controller). All converted to an explicit fixed→float render boundary: world positions difference against the draw origin in fixed (exact at any distance), then cross to float by value. The engine itself needed zero changes — uniform precision was always true; only the sample app's float boundary was lying about it.
  • Two more silent int→fixed truncations found by the new audit tooling: the character sample's rolling resistance (now matches upstream behavior) and the overflow-pile ring placement.
  • The time-of-impact root finder gains a stall early-out (outcome-identical by construction; saves ~200 wasted separation evaluations per stall on degenerate inputs).

New

  • 100 km scene origin, structurally enforced. Every sample, every benchmark scene, and the determinism test now build their content around a constant 100 km origin — with deliberately no setter, so nothing can quietly opt back to (0,0,0). The repository's uniform-precision claim is exercised by everything, all the time. Benchmark workloads were verified bit-identical to the historical layouts, so the published performance tables remain valid. The World/Far samples keep configurable offsets up to 10,000 km — where float's pyramid shatters on its ~1 m precision grid and the fixed-point one is pristine.
  • Per-sample visual A/B harness. --list-samples, --sample N --frames F --capture out.png, and a fully headless mode (no window, physics stepped on CPU, one offscreen render — ~1 s per sample instead of ~10 s). tools/capture/ carries the sweep script, the contact-sheet generator, and the float-baseline patch so the comparison is reproducible from the repo alone. First full run: 153/153 samples match the float baseline at their 120-frame end states (worst divergence 4.3/255 mean luminance). The two honest divergences are documented: a card-house equilibrium at the Q48.16 resolution floor (float stands, fixed collapses), and the Far samples diverging in fixed point's favor.
  • tools/fixed-point/conversion_audit.py, now in CI. A type-aware clang-AST audit that flags every implicit or explicit conversion between b3Fixed and float/double in both directions, plus consumer-code int→b3Fixed truncations (hertz = 5 compiles warning-free and means 5 raw ulps). It closes the -Wfloat-conversion blind spots — braced initializers, exact-integer literals, small raw values — and runs on every push in the macOS samples job.
  • Rebranded to Fixed3D in everything user-facing (window title, docs, banners). The b3* / BOX3D_* API stays Box3D-branded by design, for drop-in migration compatibility.

Docs

  • ERIN.md grew the findings from this cycle: the sample text helpers' missing printf format attributes (and the varargs UB they hide upstream), the character-sample material-id quirk, the screenshot-sweep regression net with its Metal readback trap, and a shape.h declaration nit.
  • The manual's samples page no longer claims the app uses GLFW (it is sokol + imgui).

Baseline: upstream box3d e961bfb. License: MIT, same as Box3D.