Skip to content

feat(box2d): native Go port of Box2D v3.2.0, swap physics2d off CGO - #928

Open
smsunarto wants to merge 37 commits into
mainfrom
scott/box2d-go-cardinal-ecs-ce311c
Open

feat(box2d): native Go port of Box2D v3.2.0, swap physics2d off CGO#928
smsunarto wants to merge 37 commits into
mainfrom
scott/box2d-go-cardinal-ecs-ce311c

Conversation

@smsunarto

@smsunarto smsunarto commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Ports the full Box2D v3.2.0 feature set to pure Go at pkg/box2d: rigid bodies, all 5 shape types, the TGS-soft solver with sub-stepping, all 7 joint types, sensors, continuous collision (bullets), and world queries/explosions/debug draw.
  • Float64 throughout, with package-wide FMA-safe arithmetic and ported deterministic trig so results are bit-identical across architectures (amd64/arm64) — enforced by golden hash tests gated in CI (common-go-ci.yaml determinism job).
  • Migrates pkg/plugin/physics2d off its CGO backend onto pkg/box2d:
    • Kills the package-global runtime singleton; Plugin now owns a per-instance Runtime, enabling parallel tests and multiple physics worlds per process.
    • Replaces the WorldID() uint32 CGO escape hatch with a typed Engine() *box2d.World accessor.
    • Deletes internal/cbridge and the vendored Box2D C source (third_party/box2d); the plugin now builds with CGO_ENABLED=0.
    • Validated against CGO-recorded golden traces (captured before the C backend was removed): contact/sensor event sequences and query results match exactly, body state matches within tolerance for the float64-vs-float32 backend difference.
  • Fixes a latent bug this port's fuzzer found in the ported solver: a body leaving the awake set via SetBodyType/DisableBody kept a stale body-move-event index, causing an out-of-bounds read on a later forced sleep (upstream C has the same gap, unguarded). Regression test: TestBodyMoveIndexInvalidatedOnAwakeSetExit.

Why the pure-Go port

World Engine games carry a hard requirement: every machine — each player's server, every replay, every verifier — must compute the exact same game state from the same inputs. Physics is chaotic, so a difference in the 15th decimal place on frame 1 is a visibly different game by frame 1000. When machines disagree, players feel it: rollback netcode "corrects" positions and characters teleport, replays can't be trusted, and state verification fails with no way to tell who's right.

"Why rewrite 29k lines instead of patching?"

Because both fatal problems lived outside code we could patch. First, the calculators disagreed: different CPU families (Intel/AMD vs Apple/ARM) are legally allowed to round certain fused math operations differently, and the C build used them — identical inputs produced microscopically different results per machine. That is a property of the toolchain, not a fixable bug. Second, the Go-to-C bridge charged a translation tax on every frame: 32% of each tick went to copying collision data across the border, six times the physics computation itself (5.7%). The port also unlocks what the bridge forbade: builds with no C compiler on any platform, multiple physics worlds per process, and Go's race detector, fuzzer, and profiler seeing inside the engine — the fuzzer already caught a real solver bug that still sits unguarded in upstream C.

"Wasn't the C engine faster?"

Performance is the supporting act, honestly stated. On a single core the Go engine is still behind: +2.4% at 100 bodies (small enough that it disappears into run-to-run noise), +9.8% at 500, +9.6% at 1,000, +11.6% at 5,000 — it does double-precision math where the C build did single. The widest gap is 0.6 ms on a 16.7 ms frame. Real, but not something a player can see.

The C bridge ran on a single core. The Go engine steps on all of them, and produces byte-identical results whether it runs on one core or many. On an 18-core machine, full game tick:

Scene C (single-core) Go (single-core) Go (with workers)
5,000 bodies 5.23 ms 5.84 ms (+11.6%) 5.05 ms (−3.5%)
1,000 bodies 1.000 ms 1.096 ms (+9.6%) 1.035 ms (+3.4%)
100 bodies 0.127 ms 0.130 ms (+2.4%) — (scene too small to split)

At 1,000 bodies all three are 6–7% of a frame. At 5,000 both engines take roughly a third of a frame, and the Go one with workers is the faster of the two. Small scenes stay on the single-core path by design — splitting work across cores costs more than it saves until there are enough bodies to go around.

Queries got cheaper in the big scenes that matter: raycasting a 5,000-body world is less than half the cost it was. And the Go engine allocates 27–40% less memory per tick, which a long-running server feels as less garbage-collection pressure even where the tick timings look similar.

"How do we know the Go one is actually correct?"

Not by promise — by machine. The port follows upstream Box2D v3.2.0 file-by-file. CI compiles the engine for both CPU families and compares recorded simulations bit-for-bit. One test disassembles the compiled machine code and fails the build if the divergent instruction family (FMA) appears anywhere. The same checks prove byte-identical output at every thread count.

Notable for reviewers

  • pkg/box2d is large — treat it as a faithful, mechanical port. Each source file's header comment states its Box2D upstream provenance (files carry a // Ported to Go from Box2D v3.2.0 ... line); pkg/box2d/LICENSE covers the MIT (Box2D) and zlib (ByteArena Go port, used for some seed algorithms) attributions.
  • Determinism is the core design constraint: no * expression may appear as a direct operand of +/- anywhere in the package (see math_fma.go) — this prevents FMA-instruction divergence between architectures. testdata/golden_*.json files are committed bit-pattern regression fixtures; do not regenerate them casually.
  • pkg/plugin/physics2d/test/testdata/golden_*.json are the pre-removal CGO reference traces, kept as a permanent cross-backend regression anchor (see the header comment in golden_trace_test.go for the do/don't on regenerating them).
  • Full package test suites pass under go test ./pkg/box2d/... ./pkg/plugin/physics2d/..., including with CGO_ENABLED=0. golangci-lint is clean.

Test plan

  • go build ./pkg/box2d/... ./pkg/plugin/physics2d/...
  • go test ./pkg/box2d/... ./pkg/plugin/physics2d/... -count=1 (all green, including golden hash and golden-trace tests)
  • CGO_ENABLED=0 go build/test ./pkg/plugin/physics2d/... (proves the CGO dependency is gone)
  • golangci-lint run ./pkg/box2d/... ./pkg/plugin/physics2d/... — 0 issues
  • Deterministic op-sequence fuzzing (25 seeds × 300 ops, replayed) — found and fixed one real solver bug
  • CI cross-arch (amd64/arm64) determinism job — first run happens on this PR

🤖 Generated with Claude Code

Benchmarks and profiling vs the CGO backend

Setup: Apple M5 Max (arm64, 18 cores), go1.26.5. CGO = origin/main (vendored Box2D v3.2.0 C, float32, single-threaded bridge). Pure Go = this branch, default build (no PGO — Go does not auto-apply a library's default.pgo; it is opt-in for game binaries). Same benchmark scene and driver on both sides (pkg/plugin/physics2d/test, falling-circles), -benchtime=50x -count=6, medians, significance via benchstat. The surrounding Cardinal ECS differs between the two branches, so tick totals are comparable but per-function ECS costs are not.

These numbers supersede the earlier round in this PR, which was measured before b844d470 (per-worker scratch for the continuous sweep), a02ccd3c (int32 tree links, TreeNode down to one cache line) and b30339a7 (per-tick scratch reuse in reconcile/contact gather).

Full Cardinal tick (ECS reconcile + physics step + writeback + events)

Bodies CGO Pure Go, serial Pure Go, Workers: 18
100 127.3µs 130.3µs (+2.4%, p=0.937 — not significant) — (below dispatch grains)
500 495.3µs 543.8µs (+9.8%)
1000 1.000ms 1.096ms (+9.6%) 1.035ms (+3.4%)
5000 5.233ms 5.839ms (+11.6%) 5.049ms (−3.5%)

All deltas except the 100-body row are p=0.002 (n=6). Serial float64 Go pays +2.4% to +11.6% at the tick level; the earlier "20–30%" figure in this PR predates the three commits above and is retired. The worker pool closes the remainder and beats CGO at 5000 bodies.

Caveat on this table, stated because it flatters us: the bench harness runs with Debug: true, which makes Cardinal serialize world state every tick (persistState/ToProto, ~3 ms at 5000 bodies). That work is identical on both sides and dilutes the physics difference. With Debug: false — closer to a production shard — the physics-only gap is larger:

Bodies CGO Pure Go, serial Pure Go, Workers: 18
1000 387.6µs 473.8µs (+22.2%) 415.9µs (+7.3%)
5000 2.181ms 2.777ms (+27.4%) 2.050ms (−6.0%)

So: single-core the C build is genuinely 22–27% faster on physics (float32 arithmetic and a SIMD contact solver); the worker pool closes that and wins at 5000 bodies. Memory goes the other way by a wide margin — with Debug: false at 5000 bodies CGO allocates 4.17 MB per tick to marshal contacts across the boundary, against 440 B here.

Memory moved the other way at every size — same allocation count (±0.2%), materially fewer bytes:

Bodies CGO B/op Pure Go B/op Δ
100 172.2Ki 125.8Ki −27.0%
500 880.7Ki 605.4Ki −31.3%
1000 1.857Mi 1.178Mi −36.6%
5000 9.925Mi 5.945Mi −40.1%

Engine-only (pkg/box2d, no ECS overhead)

No CGO counterpart exists at this layer, so this is informational for our side only — not a float32-vs-float64 engine comparison:

Scene Serial Workers: 18
MixedRain 100 31.5µs 31.0µs
MixedRain 500 147.8µs 154.8µs (+4.7% — dispatch overhead exceeds the gain)
MixedRain 1000 305.9µs 246.7µs (1.24×)
MixedRain 5000 1.675ms 0.940ms (1.78×)
Pyramid 194.8µs 286.0µs (+46.8%)
Jointed 22.4µs 22.0µs (−1.7%)

Pyramid is the worst case for the worker pool and worth stating plainly: one dense stack is a single constraint island, so the per-color stages have little to split while still paying every dispatch barrier. Workers is a throughput knob for large or many-island scenes, not a default — results are byte-identical either way, so setting it wrong costs time, never correctness.

For scale on what the optimizations bought at the engine layer: before b844d470, MixedRain 5000 ran 2.395ms serial / 1.086ms at 18 workers (night_base_engine.txt). An intermediate run after b844d470 alone still measured 2.375ms, so essentially the entire ~30% serial gain comes from a02ccd3c — the 64-byte tree node — and b30339a7 is wrapper-side only.

Queries (per-op, dense grid scene)

Query 100 500 1000 5000
Raycast −22.4% −15.3% +0.3% −52.4% (1759.5 → 837.1ns)
OverlapAABB −1.3% −1.9% −5.8% −22.7% (2498.5 → 1932.0ns)
CircleSweep −52.0% +28.5% (758.3 → 974.1ns) +14.6% −7.2%

Read this with the noise floor in mind: these are sub-microsecond operations and the small-scene rows swing run to run, which is the most likely reading of CircleSweep's ±50% spread across adjacent sizes. What survives is the 5000-body column — large-scene queries get materially cheaper, most likely because query results no longer cross the language boundary per candidate, though the profiles locate boundary cost in the step's contact gather rather than inside the query calls, so the mechanism is inferred rather than measured.

CPU profiles, 5000-body tick (-benchtime=400x)

CGO (pprof top; 4.81s total samples):

34.51% flat  runtime.cgocall              <- the entire C engine, opaque to Go tooling
28.69% cum   cbridge.GatherLiveContacts   <- per-tick contact extraction across the boundary
 6.44% cum   cbridge.Step                 <- the actual physics step

The boundary crossing costs ~4.5× the step itself: every tick re-marshals all live contacts through CGO, and none of the C internals are attributable in profiles.

Pure Go (2.94s total samples):

19.73% cum   box2d.(*World).Step
 9.86%       ...updateBroadPhasePairs
 8.50%       ...solve
 6.46% flat  box2d.(*DynamicTree).Query
 8.16% cum   physics2d.(*Runtime).ReconcileFromECS

Every frame is attributable to a named Go function, so the step is transparent to pprof, PGO and the race detector. In the same profile, cardinal.(*World).persistState / ecs.ToProto is 39.46% cum — on the Go side, physics is no longer the dominant tick cost. The bench harness runs with Debug enabled, which forces state serialization every tick, so that share is partly a harness artifact; it is not physics, not comparable across the two branches, and is tracked as separate follow-up work.

On determinism, stated as what is enforced rather than what is hoped: CI checks the golden traces byte-for-byte on amd64 and arm64 at WorkerCount 2/4/8, plus an instruction-level no-FMA gate that also runs under PGO codegen. The C build's cross-host behaviour was not measured here, so no claim is made about it.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Claude finished @smsunarto's task in 12m 24s —— View job


Review — round 5 (7fa4de08c9647501)

Re-reviewed the four commits since the last round. The validator wrapping (7fa4de08) is mechanically correct and the poison work now covers every read path that carries an if w.locked guard — I re-derived that set myself (grep -n -B25 "if w.locked {" over body.go/shape.go/world.go/joint.go) and the twenty remaining guards are all mutators plus one internal helper. That helper is the finding below.

Verified green on this checkout:

go test ./pkg/box2d/... -count=1                    ok  27.093s
go test -tags box2d_asserts ./pkg/box2d/... -count=1 ok  22.155s
go test ./pkg/plugin/physics2d/... -count=1          ok

1. The poison's "name the cause and remedy" contract stops at the joint accessors — they die on a nil pointer instead

getJointSimCheckType (pkg/box2d/joint.go:249-253) returns nil when w.locked, and every public joint accessor dereferences that result on the next line. Since 34ec7744 the unwind leaves w.locked latched true, so the guard is now reachable from ordinary post-recover code — the exact mechanism the last three rounds closed for the queries, event accessors and per-body getters.

Verified on this checkout, default build (scratch test: buildWorkerStressScene, revolute + prismatic joint, pre-solve callback panics once, caller recovers, same world re-read):

pre-poison RevoluteJointAngle = 0
fired = true
RevoluteJointAngle          panic=runtime error: invalid memory address or nil pointer dereference
PrismaticJointTranslation   panic=runtime error: invalid memory address or nil pointer dereference
PrismaticJointSpeed         value=0            <- no panic
JointCollideConnected       value=false        <- no panic

That is 112 call sites across distance_joint.go, motor_joint.go, prismatic_joint.go, revolute_joint.go, weld_joint.go and wheel_joint.go. It is loud, so this is a diagnostics defect rather than a silent-wrong-answer one — but it is the same wrong-message failure you fixed last round when you hoisted panicIfPoisoned above assert(!w.locked) so the tagged build would stop reporting a bare assertion failed. Here the message is worse than a bare assert: nil pointer dereference inside box2d reads as an engine bug, and it points a debugger at distance_joint.go:46 rather than at the caller's recovered tick.

PrismaticJointSpeed (prismatic_joint.go:217) is the one in that family that answers silently — it deliberately resolves the sim itself to preserve the upstream b2PrismaticJoint_GetSpeed exemption, so it skips the guard on a poisoned world too.

One line closes both:

func (w *World) getJointSimCheckType(jointID JointID, jointType JointType) *jointSim {
	w.panicIfPoisoned()
	assert(!w.locked)

plus the same first statement in PrismaticJointSpeed. The reentrant path is unaffected — stepPanicked is false mid-step, so a joint read from inside a callback keeps its documented nil-return-then-deref parity.

Fix this →


2. Lower severity — the scope comment says "every READ path", the implemented rule is "every read path that already had a locked guard"

pkg/box2d/world.go:569-576 states the contract as reads-check / mutators-don't. The actual dividing line is different: an accessor is covered iff it happened to carry an if w.locked guard. The getters that never had one still answer silently on a poisoned world — verified IsBodyAwake = true, BodyPosition = {0 40}, BodyLinearVelocity all returning with no panic on the same poisoned world above. The set includes BodyPosition / BodyRotation / BodyLinearVelocity / BodyAngularVelocity, which is exactly the four reads pkg/plugin/physics2d/internal/writeback.go:46-49 makes, plus BodyMotionLocks (read by reconcile.go:260), BodyMassData, ShapeFilter, IsShapeSensor, JointCollideConnected.

Stating the limits honestly: I could not construct a reachable failure through physics2d. The pipeline system runs reconcile → Step → flush → writeback in one function (internal/system/pipeline.go:79-118), so a Step panic unwinds before writeback, and the next tick's reconcile is followed immediately by the loud Step. And unlike the guarded set, these return last-good state rather than a zero value, so they are not the "plausible-looking value a caller culls with" case that motivated c9647501. So this is a doc-accuracy item, not a bug — but a future editor reading "every READ path" will assume BodyPosition is covered when it is not. Either extend the check to the body/shape state getters, or narrow the sentence and say why the rest are safe to leave.

Fix this →


Checked and clean this round

  • 7fa4de08 — all ~30 if debugAsserts { ... } wraps are around pure predicates with no side effects; the two ValidateHull sites keep their hull.Count < 3 release fallback intact, so asserts-off behaviour is unchanged. The else arms added to TestOracleShapeHelpersDefaultArm do pin both tiers now.
  • Step's completion-flag defer is installed after the timeStep == 0 and reentrancy early returns, so neither can poison a healthy world.
  • workerPool.forRange clears every panic slot before re-raising and drops p.fn first, so a poisoned world's pool is still safe to close()Destroy on a poisoned world works.
  • reconcileSortScratch now goes through clearScratchTail; Reset() drops all six scratch fields.
  • sortedEntriesContainID's lo+(hi-lo)/2 and the destroyOrphanBodies sort-before-destroy keep orphan handling deterministic under map iteration.
  • The no-FMA gate compiles the test binary and both PGO variants, and its mnemonic regex covers the scalar float64 FMA forms Go actually emits on arm64 and amd64/v3.

No changes pushed — this is review feedback only.
· branch scott/box2d-go-cardinal-ecs-ce311c

Comment thread .github/workflows/box2d-determinism.yaml Fixed
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
783 3 780 4
View the top 3 failed test(s) by shortest run time
github.com/argus-labs/world-engine/pkg/box2d::TestGoldenMath
Stack Traces | 0.01s run time
=== RUN   TestGoldenMath
=== PAUSE TestGoldenMath
=== CONT  TestGoldenMath
    golden_math_test.go:144: 
        	Error Trace:	.../pkg/box2d/golden_math_test.go:144
        	Error:      	Not equal: 
        	            	expected: []string{"9ea22eb81e2bf444", "1f172d73dc6f2191"}
        	            	actual  : []string{"9ea22eb81e2bf43e", "1f172d73dc6f2172"}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,4 +1,4 @@
        	            	 ([]string) (len=2) {
        	            	- (string) (len=16) "9ea22eb81e2bf444",
        	            	- (string) (len=16) "1f172d73dc6f2191"
        	            	+ (string) (len=16) "9ea22eb81e2bf43e",
        	            	+ (string) (len=16) "1f172d73dc6f2172"
        	            	 }
        	Test:       	TestGoldenMath
        	Messages:   	bit mismatch in "spring_damper": results are not bit-identical to the golden generation — likely an FMA or libm determinism leak (see math_fma.go)
--- FAIL: TestGoldenMath (0.01s)
github.com/argus-labs/world-engine/pkg/box2d::TestGoldenDistance
Stack Traces | 0.02s run time
=== RUN   TestGoldenDistance
=== PAUSE TestGoldenDistance
=== CONT  TestGoldenDistance
    golden_distance_test.go:171: 
        	Error Trace:	.../pkg/box2d/golden_distance_test.go:171
        	Error:      	Not equal: 
        	            	expected: []string{"3ff1f7bd5a889302", "3ff459fe3704a55a", "bff0560a913ff9ee", "3fe4db1e01e21431", "bfb5b7cf56f1f13a", "bfe1ac703afa8688", "3feaad39bb7f3581", "3fe3f878f83dadfa", "3fe83cfe6006f3b0", "3fe6c904bf6add86", "3fdc3e2dadfdacf6", "3fc644638c006708", "bfe035f2c910f16a", "bfeb970eec3cc58b", "3ff13c7be3d0d38a", "bfdeefe5a133fdc5", "3fd4c992286d0268", "bff76a860d638ca9", "3fe8b33f74bff914", "bfed1d36df5236b9", "3fda8f8c09a0fe12", "400c54695076aece", "c0027a441d612d47", "bfeb89cba0521435", "3ff3ad5eeb0e98b8", "bfe809d117f3d6d8", "3feffc1781d4364a", "3f9fa075adb85e10", "40085f385369d316", "3fc9f5dcb0b8a4bd", "bff7082bfe71aa02", "3feda252edc65340", "3ff85185d0993efd", "3fce633f803ddede", "3fef15cca1223235", "3ff5e1b61b1cf30b", "bfe10061ca62c072", "3fd81ff3090fb4f6", "3fd48f4e865b4bfc", "3ff7240dc8a0b868", "3fe3f2c32bfcf0b0", "3fe905743e2f83d2", "4008c74adf80cb7e", "3fff01bafef2fe9f", "bff46297f85c30c4", "3fe0a2071b80a590", "3ff7ac11dffaed51", "bfdd4da0311a9f66", "3fec72dff950388a", "400138b5d501ceca", "3fe9412241a0ad92", "bfe214dc2479e0a1", "3fd473aa94b6d7ea", "3ff892989ea9c630", "bfcbecd28a321c37", "3fef3aabb5237c44", "400c1d9d19e7fff2", "bffe251ef290093f", "3fe503014633853f", "3ff981c08c32fa10", "3fc3a13085db0879", "3fefab932c16ab61", "bfc25447bf49e786", "3ff1d84d3e3f0625", "3fc4a4f4c84dd5a8", "3fea70b50e083a4c", "3fd628d29376b7fa", "bfd182c581ffa37c", "3fc53a181d1c1730", "bfef8e91acbe4ce1", "400ae9db178977dc", "c001006726b66142", "bff6569002ec208a", "3ff3d0cd0fc94bd4", "bff74765dd5d3165", "3feffebfac53a590", "bf91e5a1619d6dc8", "0000000000000000", "3ff4478d60592c2d", "3fd2573020127d82", "3ff4478d60592c2d", "3fd2573020127d82", "0000000000000000", "0000000000000000", "4001a5b2c8f08680", "bff62aa8125cf18e", "3ff3928206448a0d", "3fea3eb208d7a9ee", "3ff2f3e22d9e1479", "3feffebccb5c511a", "bf91fa2b98c0f8c0", "3fee5ec303d6ec3c", "bffc6fa711059ae4", "bff387b93fff92ce", "bffc500bfa9ed57a", "bfd161e28a011408", "3f80a6a69a18ea90", "3fefffbaaf60fdeb", "0000000000000000", "bfb5d7aa5fc55bb5", "3fc97ee643936dee", "bfa9c3839c0e04fb", "3fb206c6fbe05f86", "bfd0c99629249b7b", "3feee128e881e3dc", "400a8f080f89adc9", "40006803befe50e0", "bff0c2aed7d1ec7e", "bfc9283188939dd0", "3ff656075299bd9c", "bfe5a97b1948c7b2", "3fe78da2ba11268e", "0000000000000000", "bffbbfb32f5febce", "bfe54f9a178a228e", "bffbdfaa07b564fe", "bfe7378f00d25cc8", "3fc0a0a53491d094", "3fefba965a245528", "400374f078d4c421", "c0003d433b63bc3a", "bff25b982bfd3e1e", "3fd96fe63ae4bc28", "bfefdd102a07d1f1", "3feff01075e68d34", "3fafeb8bc6b36590", "4006745cd70ffd22", "bffcf1c22f0a559e", "3fd1918ad268bee0", "3fe86007eb71d43d", "bfeb454cc10a5d37", "3fed4eff85d7db87", "bfd9b0e4cf35e852", "3fd95236bfba3653", "bfd35690348cfdda", "3fa645dad841d3d0", "bfe65462dd43e29f", "3fa606d44ae21800", "bfeffffe737f0ca6", "bf53e992dedeb400", "0000000000000000", "bff0faca8aa9a6fe", "3fec03308fa65b4f", "bff2afdefd33383f", "3fe91ee1a4a08514", "3fe86b6c028f56e4", "3fe4ae34083148f5", "3ff7fd2264a5d25a", "bff3626b3a2d986f", "bfcd30f43098bf88", "3f991f20997d1600", "bff139d2e0800e4a", "3fea61b0b87ec9ab", "bfe21c6f6797763a", "3feefbc40c34732c", "3fdc1f7a055d1084", "c0007450bef1bebf", "3ff534762d6be61c", "bffaa825ff5953e6", "3fed4749670d79ef", "3fd9d3f8a1e462e8", "4009eb68ac1e7b7e", "400093bad348cd20", "bff038c1df1433a8", "bfecd4c89c165c38", "3fd184c46b3b18d8", "bfed5d516f68f6b7", "3fd96f2847b2fa60", "3fe301485e35ca30", "3fe48dfeb798b266", "bff20360e0d2535d", "3fd55c8f79d31121", "bffa21f009cd125a", "bfe0a014d919be8b", "bfeb579de4065cb6", "4008334d0652c374", "bfeb87021e313f6a", "3fe6e61efac4e1f5", "3fe92bb35d8643ea", "bffd268d9bf50b64", "3fe16baa76528325", "bfead7b2e44f6568", "400bd8a1279e1ee6", "40016509b4d26710", "bfe4a95e4103707c", "bff2c4f635e877b2", "3fd3bf866590cee0", "bfeec6264e6bdbe6", "3fd18b9a22d39149", "40076772f21f51a4", "3ff5265554a3d360", "c003a9b1241fb0c0", "bfda71d960128148", "bfba36a480667600", "bfe2fa7f22f1e732", "3fe9c3bf27b51076", "3ffe309cf7f19a22", "3fe8d3549b013b11", "3fd50e1daf2cf3e2", "3fe942f1e75eba85", "bff8ece1f74200f6", "3f7d939e7c9f8900", "bfefffc95360d0cc", "3ffa22408d21402c", "3fd8bd6778ad7ac8", "bfe61c5340d73809", "bfbcadaec00a6fa0", "3feba98dac35c588", "bfd3893b8582aa7d", "3fee7902f9c7bb11", "0000000000000000", "bffec89ee0fb2974", "bf6ae8375a92c020", "bfffcdb0edc3aad6", "bf9cd83c738dc44b", "3fedcf0c6bf38c73", "3fd74682bd7b61fb", "3fe80eec7a119283", "bfdfd4fd78b8891a", "3fcef116c606e6a2", "bfc5c5730e9d4186", "bfdbd8e2e7fda69c", "3fdbdc5cdc7192f6", "bfeccf0a3af2dec2", "40035f4f1a67ffea", "bfdc1a9aa239bdb7", "c002049df683735b", "bfed87d12e64c25a", "3fbed7d7cb449326", "bfc99173f3f95b00", "3fef5ae749fe97e0", "400be2b3bf22f080", "3fe852513d10f198", "3ffab223f875b185", "3fb5078eed6b3aa8", "bffc02a793c86d2a", "bfc8e4c24a8b74cc", "bfef639581fff498", "400898e5f26fafd5", "bfc41322921a7545", "c0018cedb5e0c84d", "bff4fbf377e1d73e", "3fe4fc3be5ab1430", "bfd808f52e6a7730", "3feda85fb951e454", "3ffe0648597e2efd", "bfecac7992b6fe06", "3fe47abeb33abfce", "3fecf35adf499b36", "3fbcb3d498fe5f60", "3feeb5363483a293", "bfd200c7c2b746bc", "400240567f568226", "3fdc0bed535759cb", "3ff0aa17f8a50a44", "3ff6ec491fed81f4", "bff030169c33947c", "3fdbe5b7084750b6", "bfecccc6ea0c8ed5", "4004e62781faa24e", "3ff936bb4132696d", "3fefcb23af169cf8", "3fed9e2a599fb0a3", "bff895e75854d01f", "bfcfdd00960f079c", "bfeefe1fb3f48076", "0000000000000000", "bff562e98823bc37", "bff32c45a4572ca6", "bff5699de9c99f85", "bff2b4cc110c5ad4", "3facafdbe14ea89c", "bfeff321a5fb2cca", "4008387b9cd23748", "bff1e6d96e0a4374", "c00015be15326b55", "bff1386e8d664eea", "3ff044410a5e882e", "3f8cce05b1f4e428", "3fefff308f9c9e30", "400eb433372b8f38", "c0021e2f7c6709a3", "bffa036ed009ecf3", "3ff1899850a792a9", "3fcd1fa2aca8c2b8", "3fec0588309209ca", "3fdee7d705b2e8f9", "3feed3bb55ad4881", "bfeeaf219be301dc", "3fee4fe24c415d57", "bfc86dbd58c7fc28", "3fd768206ce9b0e8", "3fe98315f1c48e0e", "bfe35113a0eb8182", "3fe9f250aae8a266", "3fe97cf50179beb0", "bfe62c9f976c67ed", "3fe6b9126ac433b5", "bff7fc8e220aebee", "bfbb484e3cd4cd00", "bfefd158aef727ba", "400a633a53a5c38b", "bfe3c0b7bc1af918", "c0011eff32353bf8", "3ff907b1ab79554a", "3fd55c5b724e60f0", "3fe52a55329eb330", "3fe80025733d71ee", "4007e711524cba10", "bfc18b15f6169883", "3fff4e9776aaf45b", "bfd625e663f37434", "bff061906a11b91d", "bfb1e869b7f6f584", "bfefebeea47e9bb8", "40088368889ec86b", "bff8a1d7336b8d16", "3fa6d0de344e4f80", "3ff74bd2cfa73f84", "bfe33641870abf4a", "3fef487d694f3158", "bfcaf0e6f3dc4d7a", "3ff6df329ed652d4", "bffc1527ce252d3c", "3ff18afa345187ce", "bfdbabbe07a635e1", "3fe1beee4443654b", "3fed9c9ff6f03ce7", "bfd842a20f40f621", "4003d5a8da17a83d", "3ffdb006e41866ed", "bff69544f0350415", "bfe16dc16320e21c", "bfe945de6a015437", "bfeefa4de3a20edf", "3fd00c1bb8d4853a", "3ff14c70043a2511", "bfd32cbd5219525a", "3fd2d794c9b281d4", "bff1f7a12e7beec0", "3fefd80f978508fb", "bfe85e8f004bd966", "3fe4bd5ae1e58a9f", "400744d1409cdb90", "bff4ba843a4042cc", "bffdb2e3e38a5808", "bfc4751870e64950", "3fea4a0237222110", "3fd8fd68b7c269d1", "3fed75b1bf8853d4"}
        	            	actual  : []string{"3ff1f7bd5a889302", "3ff459fe3704a55a", "bff0560a913ff9ee", "3fe4db1e01e21431", "bfb5b7cf56f1f13a", "bfe1ac703afa8688", "3feaad39bb7f3581", "3fe3f878f83dadf9", "3fe83cfe6006f3b0", "3fe6c904bf6add84", "3fdc3e2dadfdacf6", "3fc644638c006706", "bfe035f2c910f16b", "bfeb970eec3cc58a", "3ff13c7be3d0d388", "bfdeefe5a133fdc5", "3fd4c992286d0268", "bff76a860d638ca9", "3fe8b33f74bff914", "bfed1d36df5236ba", "3fda8f8c09a0fe12", "400c54695076aecf", "c0027a441d612d47", "bfeb89cba0521436", "3ff3ad5eeb0e98ba", "bfe809d117f3d6da", "3feffc1781d4364a", "3f9fa075adb85df0", "40085f385369d316", "3fc9f5dcb0b8a4be", "bff7082bfe71aa02", "3feda252edc65342", "3ff85185d0993efd", "3fce633f803ddee2", "3fef15cca1223235", "3ff5e1b61b1cf30b", "bfe10061ca62c072", "3fd81ff3090fb4f6", "3fd48f4e865b4bfa", "3ff7240dc8a0b868", "3fe3f2c32bfcf0af", "3fe905743e2f83d2", "4008c74adf80cb7f", "3fff01bafef2fe9f", "bff46297f85c30c4", "3fe0a2071b80a58c", "3ff7ac11dffaed53", "bfdd4da0311a9f68", "3fec72dff9503889", "400138b5d501ceca", "3fe9412241a0ad92", "bfe214dc2479e0a1", "3fd473aa94b6d7ea", "3ff892989ea9c630", "bfcbecd28a321c37", "3fef3aabb5237c44", "400c1d9d19e7fff2", "bffe251ef2900940", "3fe5030146338540", "3ff981c08c32fa10", "3fc3a13085db0876", "3fefab932c16ab60", "bfc25447bf49e789", "3ff1d84d3e3f0625", "3fc4a4f4c84dd5ac", "3fea70b50e083a4e", "3fd628d29376b7fc", "bfd182c581ffa37c", "3fc53a181d1c172e", "bfef8e91acbe4ce2", "400ae9db178977dc", "c001006726b66142", "bff6569002ec208a", "3ff3d0cd0fc94bd4", "bff74765dd5d3165", "3feffebfac53a590", "bf91e5a1619d6dc8", "0000000000000000", "3ff4478d60592c2c", "3fd2573020127d82", "3ff4478d60592c2c", "3fd2573020127d82", "0000000000000000", "0000000000000000", "4001a5b2c8f08680", "bff62aa8125cf18e", "3ff3928206448a0d", "3fea3eb208d7a9ee", "3ff2f3e22d9e1479", "3feffebccb5c511a", "bf91fa2b98c0f8c0", "3fee5ec303d6ec3d", "bffc6fa711059ae4", "bff387b93fff92ce", "bffc500bfa9ed579", "bfd161e28a011404", "3f80a6a69a18eaa0", "3fefffbaaf60fdec", "0000000000000000", "bfb5d7aa5fc55bd0", "3fc97ee643936dec", "bfa9c3839c0e0531", "3fb206c6fbe05f88", "bfd0c99629249b7a", "3feee128e881e3db", "400a8f080f89adc9", "40006803befe50e0", "bff0c2aed7d1ec7e", "bfc9283188939dd0", "3ff656075299bd9c", "bfe5a97b1948c7b2", "3fe78da2ba11268e", "0000000000000000", "bffbbfb32f5febce", "bfe54f9a178a228e", "bffbdfaa07b564fe", "bfe7378f00d25cc8", "3fc0a0a53491d096", "3fefba965a245528", "400374f078d4c422", "c0003d433b63bc3a", "bff25b982bfd3e1e", "3fd96fe63ae4bc2c", "bfefdd102a07d1f9", "3feff01075e68d34", "3fafeb8bc6b36568", "4006745cd70ffd21", "bffcf1c22f0a559e", "3fd1918ad268bedf", "3fe86007eb71d43d", "bfeb454cc10a5d37", "3fed4eff85d7db86", "bfd9b0e4cf35e855", "3fd95236bfba3652", "bfd35690348cfddb", "3fa645dad841d3e0", "bfe65462dd43e29f", "3fa606d44ae21800", "bfeffffe737f0ca4", "bf53e992dedebb00", "0000000000000000", "bff0faca8aa9a6fd", "3fec03308fa65b4f", "bff2afdefd33383f", "3fe91ee1a4a08514", "3fe86b6c028f56e5", "3fe4ae34083148f5", "3ff7fd2264a5d25b", "bff3626b3a2d9870", "bfcd30f43098bf90", "3f991f20997d1640", "bff139d2e0800e49", "3fea61b0b87ec9ae", "bfe21c6f67977636", "3feefbc40c34732c", "3fdc1f7a055d1084", "c0007450bef1bebf", "3ff534762d6be61c", "bffaa825ff5953e6", "3fed4749670d79ef", "3fd9d3f8a1e462e8", "4009eb68ac1e7b7f", "400093bad348cd20", "bff038c1df1433aa", "bfecd4c89c165c3c", "3fd184c46b3b18de", "bfed5d516f68f6b8", "3fd96f2847b2fa62", "3fe301485e35ca32", "3fe48dfeb798b26c", "bff20360e0d2535b", "3fd55c8f79d31134", "bffa21f009cd125b", "bfe0a014d919be83", "bfeb579de4065cba", "4008334d0652c373", "bfeb87021e313f6a", "3fe6e61efac4e1f5", "3fe92bb35d8643ea", "bffd268d9bf50b62", "3fe16baa76528325", "bfead7b2e44f6566", "400bd8a1279e1ee6", "40016509b4d26710", "bfe4a95e4103707c", "bff2c4f635e877b2", "3fd3bf866590cee0", "bfeec6264e6bdbe6", "3fd18b9a22d39149", "40076772f21f51a4", "3ff5265554a3d360", "c003a9b1241fb0c0", "bfda71d960128148", "bfba36a480667600", "bfe2fa7f22f1e732", "3fe9c3bf27b51076", "3ffe309cf7f19a22", "3fe8d3549b013b11", "3fd50e1daf2cf3e4", "3fe942f1e75eba83", "bff8ece1f74200f8", "3f7d939e7c9f88c0", "bfefffc95360d0ce", "3ffa22408d21402b", "3fd8bd6778ad7ac8", "bfe61c5340d73808", "bfbcadaec00a6fa0", "3feba98dac35c586", "bfd3893b8582aa7c", "3fee7902f9c7bb12", "0000000000000000", "bffec89ee0fb2975", "bf6ae8375a92bf20", "bfffcdb0edc3aad7", "bf9cd83c738dc435", "3fedcf0c6bf38c71", "3fd74682bd7b6207", "3fe80eec7a119282", "bfdfd4fd78b8891a", "3fcef116c606e6a2", "bfc5c5730e9d4186", "bfdbd8e2e7fda69a", "3fdbdc5cdc7192f9", "bfeccf0a3af2dec2", "40035f4f1a67ffea", "bfdc1a9aa239bdb7", "c002049df683735b", "bfed87d12e64c25a", "3fbed7d7cb449326", "bfc99173f3f95b00", "3fef5ae749fe97e0", "400be2b3bf22f081", "3fe852513d10f198", "3ffab223f875b185", "3fb5078eed6b3a80", "bffc02a793c86d2c", "bfc8e4c24a8b74d0", "bfef639581fff49a", "400898e5f26fafd5", "bfc41322921a7545", "c0018cedb5e0c84d", "bff4fbf377e1d73d", "3fe4fc3be5ab1430", "bfd808f52e6a7730", "3feda85fb951e454", "3ffe0648597e2efe", "bfecac7992b6fe06", "3fe47abeb33abfce", "3fecf35adf499b38", "3fbcb3d498fe5f50", "3feeb5363483a292", "bfd200c7c2b746bf", "400240567f568226", "3fdc0bed535759c8", "3ff0aa17f8a50a44", "3ff6ec491fed81f2", "bff030169c33947c", "3fdbe5b7084750b3", "bfecccc6ea0c8ed5", "4004e62781faa24e", "3ff936bb4132696d", "3fefcb23af169cf8", "3fed9e2a599fb0a3", "bff895e75854d01f", "bfcfdd00960f079c", "bfeefe1fb3f48076", "0000000000000000", "bff562e98823bc37", "bff32c45a4572ca6", "bff5699de9c99f85", "bff2b4cc110c5ad4", "3facafdbe14ea89c", "bfeff321a5fb2cca", "4008387b9cd23749", "bff1e6d96e0a4374", "c00015be15326b56", "bff1386e8d664eea", "3ff044410a5e8830", "3f8cce05b1f4e430", "3fefff308f9c9e30", "400eb433372b8f38", "c0021e2f7c6709a3", "bffa036ed009ecf3", "3ff1899850a792a9", "3fcd1fa2aca8c2b8", "3fec0588309209ca", "3fdee7d705b2e8f9", "3feed3bb55ad4880", "bfeeaf219be301dc", "3fee4fe24c415d56", "bfc86dbd58c7fc30", "3fd768206ce9b0e8", "3fe98315f1c48e0e", "bfe35113a0eb8183", "3fe9f250aae8a268", "3fe97cf50179beb0", "bfe62c9f976c67ec", "3fe6b9126ac433b5", "bff7fc8e220aebee", "bfbb484e3cd4cd00", "bfefd158aef727ba", "400a633a53a5c38b", "bfe3c0b7bc1af918", "c0011eff32353bf8", "3ff907b1ab79554a", "3fd55c5b724e60f0", "3fe52a55329eb330", "3fe80025733d71ee", "4007e711524cba10", "bfc18b15f6169883", "3fff4e9776aaf45b", "bfd625e663f37434", "bff061906a11b91d", "bfb1e869b7f6f584", "bfefebeea47e9bb8", "40088368889ec86b", "bff8a1d7336b8d16", "3fa6d0de344e4f60", "3ff74bd2cfa73f86", "bfe33641870abf4c", "3fef487d694f3158", "bfcaf0e6f3dc4d7c", "3ff6df329ed652d4", "bffc1527ce252d3c", "3ff18afa345187ce", "bfdbabbe07a635e1", "3fe1beee4443654d", "3fed9c9ff6f03ce8", "bfd842a20f40f61f", "4003d5a8da17a83d", "3ffdb006e41866ed", "bff69544f0350415", "bfe16dc16320e21c", "bfe945de6a015437", "bfeefa4de3a20edf", "3fd00c1bb8d4853a", "3ff14c70043a2511", "bfd32cbd5219525a", "3fd2d794c9b281d4", "bff1f7a12e7beec0", "3fefd80f978508fb", "bfe85e8f004bd966", "3fe4bd5ae1e58a9f", "400744d1409cdb90", "bff4ba843a4042cc", "bffdb2e3e38a5809", "bfc4751870e64950", "3fea4a0237222112", "3fd8fd68b7c269d0", "3fed75b1bf8853d4"}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -8,10 +8,10 @@
        	            	  (string) (len=16) "3feaad39bb7f3581",
        	            	- (string) (len=16) "3fe3f878f83dadfa",
        	            	+ (string) (len=16) "3fe3f878f83dadf9",
        	            	  (string) (len=16) "3fe83cfe6006f3b0",
        	            	- (string) (len=16) "3fe6c904bf6add86",
        	            	+ (string) (len=16) "3fe6c904bf6add84",
        	            	  (string) (len=16) "3fdc3e2dadfdacf6",
        	            	- (string) (len=16) "3fc644638c006708",
        	            	- (string) (len=16) "bfe035f2c910f16a",
        	            	- (string) (len=16) "bfeb970eec3cc58b",
        	            	- (string) (len=16) "3ff13c7be3d0d38a",
        	            	+ (string) (len=16) "3fc644638c006706",
        	            	+ (string) (len=16) "bfe035f2c910f16b",
        	            	+ (string) (len=16) "bfeb970eec3cc58a",
        	            	+ (string) (len=16) "3ff13c7be3d0d388",
        	            	  (string) (len=16) "bfdeefe5a133fdc5",
        	            	@@ -20,17 +20,17 @@
        	            	  (string) (len=16) "3fe8b33f74bff914",
        	            	- (string) (len=16) "bfed1d36df5236b9",
        	            	+ (string) (len=16) "bfed1d36df5236ba",
        	            	  (string) (len=16) "3fda8f8c09a0fe12",
        	            	- (string) (len=16) "400c54695076aece",
        	            	+ (string) (len=16) "400c54695076aecf",
        	            	  (string) (len=16) "c0027a441d612d47",
        	            	- (string) (len=16) "bfeb89cba0521435",
        	            	- (string) (len=16) "3ff3ad5eeb0e98b8",
        	            	- (string) (len=16) "bfe809d117f3d6d8",
        	            	+ (string) (len=16) "bfeb89cba0521436",
        	            	+ (string) (len=16) "3ff3ad5eeb0e98ba",
        	            	+ (string) (len=16) "bfe809d117f3d6da",
        	            	  (string) (len=16) "3feffc1781d4364a",
        	            	- (string) (len=16) "3f9fa075adb85e10",
        	            	+ (string) (len=16) "3f9fa075adb85df0",
        	            	  (string) (len=16) "40085f385369d316",
        	            	- (string) (len=16) "3fc9f5dcb0b8a4bd",
        	            	+ (string) (len=16) "3fc9f5dcb0b8a4be",
        	            	  (string) (len=16) "bff7082bfe71aa02",
        	            	- (string) (len=16) "3feda252edc65340",
        	            	+ (string) (len=16) "3feda252edc65342",
        	            	  (string) (len=16) "3ff85185d0993efd",
        	            	- (string) (len=16) "3fce633f803ddede",
        	            	+ (string) (len=16) "3fce633f803ddee2",
        	            	  (string) (len=16) "3fef15cca1223235",
        	            	@@ -39,13 +39,13 @@
        	            	  (string) (len=16) "3fd81ff3090fb4f6",
        	            	- (string) (len=16) "3fd48f4e865b4bfc",
        	            	+ (string) (len=16) "3fd48f4e865b4bfa",
        	            	  (string) (len=16) "3ff7240dc8a0b868",
        	            	- (string) (len=16) "3fe3f2c32bfcf0b0",
        	            	+ (string) (len=16) "3fe3f2c32bfcf0af",
        	            	  (string) (len=16) "3fe905743e2f83d2",
        	            	- (string) (len=16) "4008c74adf80cb7e",
        	            	+ (string) (len=16) "4008c74adf80cb7f",
        	            	  (string) (len=16) "3fff01bafef2fe9f",
        	            	  (string) (len=16) "bff46297f85c30c4",
        	            	- (string) (len=16) "3fe0a2071b80a590",
        	            	- (string) (len=16) "3ff7ac11dffaed51",
        	            	- (string) (len=16) "bfdd4da0311a9f66",
        	            	- (string) (len=16) "3fec72dff950388a",
        	            	+ (string) (len=16) "3fe0a2071b80a58c",
        	            	+ (string) (len=16) "3ff7ac11dffaed53",
        	            	+ (string) (len=16) "bfdd4da0311a9f68",
        	            	+ (string) (len=16) "3fec72dff9503889",
        	            	  (string) (len=16) "400138b5d501ceca",
        	            	@@ -58,15 +58,15 @@
        	            	  (string) (len=16) "400c1d9d19e7fff2",
        	            	- (string) (len=16) "bffe251ef290093f",
        	            	- (string) (len=16) "3fe503014633853f",
        	            	+ (string) (len=16) "bffe251ef2900940",
        	            	+ (string) (len=16) "3fe5030146338540",
        	            	  (string) (len=16) "3ff981c08c32fa10",
        	            	- (string) (len=16) "3fc3a13085db0879",
        	            	- (string) (len=16) "3fefab932c16ab61",
        	            	- (string) (len=16) "bfc25447bf49e786",
        	            	+ (string) (len=16) "3fc3a13085db0876",
        	            	+ (string) (len=16) "3fefab932c16ab60",
        	            	+ (string) (len=16) "bfc25447bf49e789",
        	            	  (string) (len=16) "3ff1d84d3e3f0625",
        	            	- (string) (len=16) "3fc4a4f4c84dd5a8",
        	            	- (string) (len=16) "3fea70b50e083a4c",
        	            	- (string) (len=16) "3fd628d29376b7fa",
        	            	+ (string) (len=16) "3fc4a4f4c84dd5ac",
        	            	+ (string) (len=16) "3fea70b50e083a4e",
        	            	+ (string) (len=16) "3fd628d29376b7fc",
        	            	  (string) (len=16) "bfd182c581ffa37c",
        	            	- (string) (len=16) "3fc53a181d1c1730",
        	            	- (string) (len=16) "bfef8e91acbe4ce1",
        	            	+ (string) (len=16) "3fc53a181d1c172e",
        	            	+ (string) (len=16) "bfef8e91acbe4ce2",
        	            	  (string) (len=16) "400ae9db178977dc",
        	            	@@ -79,5 +79,5 @@
        	            	  (string) (len=16) "0000000000000000",
        	            	- (string) (len=16) "3ff4478d60592c2d",
        	            	+ (string) (len=16) "3ff4478d60592c2c",
        	            	  (string) (len=16) "3fd2573020127d82",
        	            	- (string) (len=16) "3ff4478d60592c2d",
        	            	+ (string) (len=16) "3ff4478d60592c2c",
        	            	  (string) (len=16) "3fd2573020127d82",
        	            	@@ -92,16 +92,16 @@
        	            	  (string) (len=16) "bf91fa2b98c0f8c0",
        	            	- (string) (len=16) "3fee5ec303d6ec3c",
        	            	+ (string) (len=16) "3fee5ec303d6ec3d",
        	            	  (string) (len=16) "bffc6fa711059ae4",
        	            	  (string) (len=16) "bff387b93fff92ce",
        	            	- (string) (len=16) "bffc500bfa9ed57a",
        	            	- (string) (len=16) "bfd161e28a011408",
        	            	- (string) (len=16) "3f80a6a69a18ea90",
        	            	- (string) (len=16) "3fefffbaaf60fdeb",
        	            	- (string) (len=16) "0000000000000000",
        	            	- (string) (len=16) "bfb5d7aa5fc55bb5",
        	            	- (string) (len=16) "3fc97ee643936dee",
        	            	- (string) (len=16) "bfa9c3839c0e04fb",
        	            	- (string) (len=16) "3fb206c6fbe05f86",
        	            	- (string) (len=16) "bfd0c99629249b7b",
        	            	- (string) (len=16) "3feee128e881e3dc",
        	            	+ (string) (len=16) "bffc500bfa9ed579",
        	            	+ (string) (len=16) "bfd161e28a011404",
        	            	+ (string) (len=16) "3f80a6a69a18eaa0",
        	            	+ (string) (len=16) "3fefffbaaf60fdec",
        	            	+ (string) (len=16) "0000000000000000",
        	            	+ (string) (len=16) "bfb5d7aa5fc55bd0",
        	            	+ (string) (len=16) "3fc97ee643936dec",
        	            	+ (string) (len=16) "bfa9c3839c0e0531",
        	            	+ (string) (len=16) "3fb206c6fbe05f88",
        	            	+ (string) (len=16) "bfd0c99629249b7a",
        	            	+ (string) (len=16) "3feee128e881e3db",
        	            	  (string) (len=16) "400a8f080f89adc9",
        	            	@@ -118,27 +118,27 @@
        	            	  (string) (len=16) "bfe7378f00d25cc8",
        	            	- (string) (len=16) "3fc0a0a53491d094",
        	            	+ (string) (len=16) "3fc0a0a53491d096",
        	            	  (string) (len=16) "3fefba965a245528",
        	            	- (string) (len=16) "400374f078d4c421",
        	            	+ (string) (len=16) "400374f078d4c422",
        	            	  (string) (len=16) "c0003d433b63bc3a",
        	            	  (string) (len=16) "bff25b982bfd3e1e",
        	            	- (string) (len=16) "3fd96fe63ae4bc28",
        	            	- (string) (len=16) "bfefdd102a07d1f1",
        	            	+ (string) (len=16) "3fd96fe63ae4bc2c",
        	            	+ (string) (len=16) "bfefdd102a07d1f9",
        	            	  (string) (len=16) "3feff01075e68d34",
        	            	- (string) (len=16) "3fafeb8bc6b36590",
        	            	- (string) (len=16) "4006745cd70ffd22",
        	            	+ (string) (len=16) "3fafeb8bc6b36568",
        	            	+ (string) (len=16) "4006745cd70ffd21",
        	            	  (string) (len=16) "bffcf1c22f0a559e",
        	            	- (string) (len=16) "3fd1918ad268bee0",
        	            	+ (string) (len=16) "3fd1918ad268bedf",
        	            	  (string) (len=16) "3fe86007eb71d43d",
        	            	  (string) (len=16) "bfeb454cc10a5d37",
        	            	- (string) (len=16) "3fed4eff85d7db87",
        	            	- (string) (len=16) "bfd9b0e4cf35e852",
        	            	- (string) (len=16) "3fd95236bfba3653",
        	            	- (string) (len=16) "bfd35690348cfdda",
        	            	- (string) (len=16) "3fa645dad841d3d0",
        	            	+ (string) (len=16) "3fed4eff85d7db86",
        	            	+ (string) (len=16) "bfd9b0e4cf35e855",
        	            	+ (string) (len=16) "3fd95236bfba3652",
        	            	+ (string) (len=16) "bfd35690348cfddb",
        	            	+ (string) (len=16) "3fa645dad841d3e0",
        	            	  (string) (len=16) "bfe65462dd43e29f",
        	            	  (string) (len=16) "3fa606d44ae21800",
        	            	- (string) (len=16) "bfeffffe737f0ca6",
        	            	- (string) (len=16) "bf53e992dedeb400",
        	            	- (string) (len=16) "0000000000000000",
        	            	- (string) (len=16) "bff0faca8aa9a6fe",
        	            	+ (string) (len=16) "bfeffffe737f0ca4",
        	            	+ (string) (len=16) "bf53e992dedebb00",
        	            	+ (string) (len=16) "0000000000000000",
        	            	+ (string) (len=16) "bff0faca8aa9a6fd",
        	            	  (string) (len=16) "3fec03308fa65b4f",
        	            	@@ -146,11 +146,11 @@
        	            	  (string) (len=16) "3fe91ee1a4a08514",
        	            	- (string) (len=16) "3fe86b6c028f56e4",
        	            	+ (string) (len=16) "3fe86b6c028f56e5",
        	            	  (string) (len=16) "3fe4ae34083148f5",
        	            	- (string) (len=16) "3ff7fd2264a5d25a",
        	            	- (string) (len=16) "bff3626b3a2d986f",
        	            	- (string) (len=16) "bfcd30f43098bf88",
        	            	- (string) (len=16) "3f991f20997d1600",
        	            	- (string) (len=16) "bff139d2e0800e4a",
        	            	- (string) (len=16) "3fea61b0b87ec9ab",
        	            	- (string) (len=16) "bfe21c6f6797763a",
        	            	+ (string) (len=16) "3ff7fd2264a5d25b",
        	            	+ (string) (len=16) "bff3626b3a2d9870",
        	            	+ (string) (len=16) "bfcd30f43098bf90",
        	            	+ (string) (len=16) "3f991f20997d1640",
        	            	+ (string) (len=16) "bff139d2e0800e49",
        	            	+ (string) (len=16) "3fea61b0b87ec9ae",
        	            	+ (string) (len=16) "bfe21c6f67977636",
        	            	  (string) (len=16) "3feefbc40c34732c",
        	            	@@ -162,17 +162,17 @@
        	            	  (string) (len=16) "3fd9d3f8a1e462e8",
        	            	- (string) (len=16) "4009eb68ac1e7b7e",
        	            	+ (string) (len=16) "4009eb68ac1e7b7f",
        	            	  (string) (len=16) "400093bad348cd20",
        	            	- (string) (len=16) "bff038c1df1433a8",
        	            	- (string) (len=16) "bfecd4c89c165c38",
        	            	- (string) (len=16) "3fd184c46b3b18d8",
        	            	- (string) (len=16) "bfed5d516f68f6b7",
        	            	- (string) (len=16) "3fd96f2847b2fa60",
        	            	- (string) (len=16) "3fe301485e35ca30",
        	            	- (string) (len=16) "3fe48dfeb798b266",
        	            	- (string) (len=16) "bff20360e0d2535d",
        	            	- (string) (len=16) "3fd55c8f79d31121",
        	            	- (string) (len=16) "bffa21f009cd125a",
        	            	- (string) (len=16) "bfe0a014d919be8b",
        	            	- (string) (len=16) "bfeb579de4065cb6",
        	            	- (string) (len=16) "4008334d0652c374",
        	            	+ (string) (len=16) "bff038c1df1433aa",
        	            	+ (string) (len=16) "bfecd4c89c165c3c",
        	            	+ (string) (len=16) "3fd184c46b3b18de",
        	            	+ (string) (len=16) "bfed5d516f68f6b8",
        	            	+ (string) (len=16) "3fd96f2847b2fa62",
        	            	+ (string) (len=16) "3fe301485e35ca32",
        	            	+ (string) (len=16) "3fe48dfeb798b26c",
        	            	+ (string) (len=16) "bff20360e0d2535b",
        	            	+ (string) (len=16) "3fd55c8f79d31134",
        	            	+ (string) (len=16) "bffa21f009cd125b",
        	            	+ (string) (len=16) "bfe0a014d919be83",
        	            	+ (string) (len=16) "bfeb579de4065cba",
        	            	+ (string) (len=16) "4008334d0652c373",
        	            	  (string) (len=16) "bfeb87021e313f6a",
        	            	@@ -180,5 +180,5 @@
        	            	  (string) (len=16) "3fe92bb35d8643ea",
        	            	- (string) (len=16) "bffd268d9bf50b64",
        	            	+ (string) (len=16) "bffd268d9bf50b62",
        	            	  (string) (len=16) "3fe16baa76528325",
        	            	- (string) (len=16) "bfead7b2e44f6568",
        	            	+ (string) (len=16) "bfead7b2e44f6566",
        	            	  (string) (len=16) "400bd8a1279e1ee6",
        	            	@@ -199,22 +199,22 @@
        	            	  (string) (len=16) "3fe8d3549b013b11",
        	            	- (string) (len=16) "3fd50e1daf2cf3e2",
        	            	- (string) (len=16) "3fe942f1e75eba85",
        	            	- (string) (len=16) "bff8ece1f74200f6",
        	            	- (string) (len=16) "3f7d939e7c9f8900",
        	            	- (string) (len=16) "bfefffc95360d0cc",
        	            	- (string) (len=16) "3ffa22408d21402c",
        	            	+ (string) (len=16) "3fd50e1daf2cf3e4",
        	            	+ (string) (len=16) "3fe942f1e75eba83",
        	            	+ (string) (len=16) "bff8ece1f74200f8",
        	            	+ (string) (len=16) "3f7d939e7c9f88c0",
        	            	+ (string) (len=16) "bfefffc95360d0ce",
        	            	+ (string) (len=16) "3ffa22408d21402b",
        	            	  (string) (len=16) "3fd8bd6778ad7ac8",
        	            	- (string) (len=16) "bfe61c5340d73809",
        	            	+ (string) (len=16) "bfe61c5340d73808",
        	            	  (string) (len=16) "bfbcadaec00a6fa0",
        	            	- (string) (len=16) "3feba98dac35c588",
        	            	- (string) (len=16) "bfd3893b8582aa7d",
        	            	- (string) (len=16) "3fee7902f9c7bb11",
        	            	- (string) (len=16) "0000000000000000",
        	            	- (string) (len=16) "bffec89ee0fb2974",
        	            	- (string) (len=16) "bf6ae8375a92c020",
        	            	- (string) (len=16) "bfffcdb0edc3aad6",
        	            	- (string) (len=16) "bf9cd83c738dc44b",
        	            	- (string) (len=16) "3fedcf0c6bf38c73",
        	            	- (string) (len=16) "3fd74682bd7b61fb",
        	            	- (string) (len=16) "3fe80eec7a119283",
        	            	+ (string) (len=16) "3feba98dac35c586",
        	            	+ (string) (len=16) "bfd3893b8582aa7c",
        	            	+ (string) (len=16) "3fee7902f9c7bb12",
        	            	+ (string) (len=16) "0000000000000000",
        	            	+ (string) (len=16) "bffec89ee0fb2975",
        	            	+ (string) (len=16) "bf6ae8375a92bf20",
        	            	+ (string) (len=16) "bfffcdb0edc3aad7",
        	            	+ (string) (len=16) "bf9cd83c738dc435",
        	            	+ (string) (len=16) "3fedcf0c6bf38c71",
        	            	+ (string) (len=16) "3fd74682bd7b6207",
        	            	+ (string) (len=16) "3fe80eec7a119282",
        	            	  (string) (len=16) "bfdfd4fd78b8891a",
        	            	@@ -222,4 +222,4 @@
        	            	  (string) (len=16) "bfc5c5730e9d4186",
        	            	- (string) (len=16) "bfdbd8e2e7fda69c",
        	            	- (string) (len=16) "3fdbdc5cdc7192f6",
        	            	+ (string) (len=16) "bfdbd8e2e7fda69a",
        	            	+ (string) (len=16) "3fdbdc5cdc7192f9",
        	            	  (string) (len=16) "bfeccf0a3af2dec2",
        	            	@@ -232,9 +232,9 @@
        	            	  (string) (len=16) "3fef5ae749fe97e0",
        	            	- (string) (len=16) "400be2b3bf22f080",
        	            	+ (string) (len=16) "400be2b3bf22f081",
        	            	  (string) (len=16) "3fe852513d10f198",
        	            	  (string) (len=16) "3ffab223f875b185",
        	            	- (string) (len=16) "3fb5078eed6b3aa8",
        	            	- (string) (len=16) "bffc02a793c86d2a",
        	            	- (string) (len=16) "bfc8e4c24a8b74cc",
        	            	- (string) (len=16) "bfef639581fff498",
        	            	+ (string) (len=16) "3fb5078eed6b3a80",
        	            	+ (string) (len=16) "bffc02a793c86d2c",
        	            	+ (string) (len=16) "bfc8e4c24a8b74d0",
        	            	+ (string) (len=16) "bfef639581fff49a",
        	            	  (string) (len=16) "400898e5f26fafd5",
        	            	@@ -242,3 +242,3 @@
        	            	  (string) (len=16) "c0018cedb5e0c84d",
        	            	- (string) (len=16) "bff4fbf377e1d73e",
        	            	+ (string) (len=16) "bff4fbf377e1d73d",
        	            	  (string) (len=16) "3fe4fc3be5ab1430",
        	            	@@ -246,15 +246,15 @@
        	            	  (string) (len=16) "3feda85fb951e454",
        	            	- (string) (len=16) "3ffe0648597e2efd",
        	            	+ (string) (len=16) "3ffe0648597e2efe",
        	            	  (string) (len=16) "bfecac7992b6fe06",
        	            	  (string) (len=16) "3fe47abeb33abfce",
        	            	- (string) (len=16) "3fecf35adf499b36",
        	            	- (string) (len=16) "3fbcb3d498fe5f60",
        	            	- (string) (len=16) "3feeb5363483a293",
        	            	- (string) (len=16) "bfd200c7c2b746bc",
        	            	+ (string) (len=16) "3fecf35adf499b38",
        	            	+ (string) (len=16) "3fbcb3d498fe5f50",
        	            	+ (string) (len=16) "3feeb5363483a292",
        	            	+ (string) (len=16) "bfd200c7c2b746bf",
        	            	  (string) (len=16) "400240567f568226",
        	            	- (string) (len=16) "3fdc0bed535759cb",
        	            	+ (string) (len=16) "3fdc0bed535759c8",
        	            	  (string) (len=16) "3ff0aa17f8a50a44",
        	            	- (string) (len=16) "3ff6ec491fed81f4",
        	            	+ (string) (len=16) "3ff6ec491fed81f2",
        	            	  (string) (len=16) "bff030169c33947c",
        	            	- (string) (len=16) "3fdbe5b7084750b6",
        	            	+ (string) (len=16) "3fdbe5b7084750b3",
        	            	  (string) (len=16) "bfecccc6ea0c8ed5",
        	            	@@ -274,8 +274,8 @@
        	            	  (string) (len=16) "bfeff321a5fb2cca",
        	            	- (string) (len=16) "4008387b9cd23748",
        	            	+ (string) (len=16) "4008387b9cd23749",
        	            	  (string) (len=16) "bff1e6d96e0a4374",
        	            	- (string) (len=16) "c00015be15326b55",
        	            	+ (string) (len=16) "c00015be15326b56",
        	            	  (string) (len=16) "bff1386e8d664eea",
        	            	- (string) (len=16) "3ff044410a5e882e",
        	            	- (string) (len=16) "3f8cce05b1f4e428",
        	            	+ (string) (len=16) "3ff044410a5e8830",
        	            	+ (string) (len=16) "3f8cce05b1f4e430",
        	            	  (string) (len=16) "3fefff308f9c9e30",
        	            	@@ -288,12 +288,12 @@
        	            	  (string) (len=16) "3fdee7d705b2e8f9",
        	            	- (string) (len=16) "3feed3bb55ad4881",
        	            	+ (string) (len=16) "3feed3bb55ad4880",
        	            	  (string) (len=16) "bfeeaf219be301dc",
        	            	- (string) (len=16) "3fee4fe24c415d57",
        	            	- (string) (len=16) "bfc86dbd58c7fc28",
        	            	+ (string) (len=16) "3fee4fe24c415d56",
        	            	+ (string) (len=16) "bfc86dbd58c7fc30",
        	            	  (string) (len=16) "3fd768206ce9b0e8",
        	            	  (string) (len=16) "3fe98315f1c48e0e",
        	            	- (string) (len=16) "bfe35113a0eb8182",
        	            	- (string) (len=16) "3fe9f250aae8a266",
        	            	+ (string) (len=16) "bfe35113a0eb8183",
        	            	+ (string) (len=16) "3fe9f250aae8a268",
        	            	  (string) (len=16) "3fe97cf50179beb0",
        	            	- (string) (len=16) "bfe62c9f976c67ed",
        	            	+ (string) (len=16) "bfe62c9f976c67ec",
        	            	  (string) (len=16) "3fe6b9126ac433b5",
        	            	@@ -318,7 +318,7 @@
        	            	  (string) (len=16) "bff8a1d7336b8d16",
        	            	- (string) (len=16) "3fa6d0de344e4f80",
        	            	- (string) (len=16) "3ff74bd2cfa73f84",
        	            	- (string) (len=16) "bfe33641870abf4a",
        	            	+ (string) (len=16) "3fa6d0de344e4f60",
        	            	+ (string) (len=16) "3ff74bd2cfa73f86",
        	            	+ (string) (len=16) "bfe33641870abf4c",
        	            	  (string) (len=16) "3fef487d694f3158",
        	            	- (string) (len=16) "bfcaf0e6f3dc4d7a",
        	            	+ (string) (len=16) "bfcaf0e6f3dc4d7c",
        	            	  (string) (len=16) "3ff6df329ed652d4",
        	            	@@ -327,5 +327,5 @@
        	            	  (string) (len=16) "bfdbabbe07a635e1",
        	            	- (string) (len=16) "3fe1beee4443654b",
        	            	- (string) (len=16) "3fed9c9ff6f03ce7",
        	            	- (string) (len=16) "bfd842a20f40f621",
        	            	+ (string) (len=16) "3fe1beee4443654d",
        	            	+ (string) (len=16) "3fed9c9ff6f03ce8",
        	            	+ (string) (len=16) "bfd842a20f40f61f",
        	            	  (string) (len=16) "4003d5a8da17a83d",
        	            	@@ -346,6 +346,6 @@
        	            	  (string) (len=16) "bff4ba843a4042cc",
        	            	- (string) (len=16) "bffdb2e3e38a5808",
        	            	+ (string) (len=16) "bffdb2e3e38a5809",
        	            	  (string) (len=16) "bfc4751870e64950",
        	            	- (string) (len=16) "3fea4a0237222110",
        	            	- (string) (len=16) "3fd8fd68b7c269d1",
        	            	+ (string) (len=16) "3fea4a0237222112",
        	            	+ (string) (len=16) "3fd8fd68b7c269d0",
        	            	  (string) (len=16) "3fed75b1bf8853d4"
        	Test:       	TestGoldenDistance
        	Messages:   	bit mismatch in "shape_distance_table": results are not bit-identical to the golden generation — likely an FMA or libm determinism leak (see math_fma.go)
--- FAIL: TestGoldenDistance (0.02s)
github.com/argus-labs/world-engine/pkg/box2d::TestGoldenStep
Stack Traces | 0.08s run time
=== RUN   TestGoldenStep
    golden_step_test.go:198: 
        	Error Trace:	.../pkg/box2d/golden_step_test.go:198
        	Error:      	Not equal: 
        	            	expected: []box2d_test.goldenStepHash{box2d_test.goldenStepHash{Step:30, Hash:"b98fd6056df1b2af"}, box2d_test.goldenStepHash{Step:60, Hash:"f9dddf09b7acf106"}, box2d_test.goldenStepHash{Step:90, Hash:"1641c1b8a5940574"}, box2d_test.goldenStepHash{Step:120, Hash:"d0fa87101552af9b"}, box2d_test.goldenStepHash{Step:150, Hash:"4b8f31adcb69d63f"}, box2d_test.goldenStepHash{Step:180, Hash:"79b7abfcb65885e1"}, box2d_test.goldenStepHash{Step:210, Hash:"483091497c152cde"}, box2d_test.goldenStepHash{Step:240, Hash:"1b6f00e55f044391"}}
        	            	actual  : []box2d_test.goldenStepHash{box2d_test.goldenStepHash{Step:30, Hash:"a2be51d8d301ad55"}, box2d_test.goldenStepHash{Step:60, Hash:"04d685515a3d5ecb"}, box2d_test.goldenStepHash{Step:90, Hash:"41f6f426dbd03462"}, box2d_test.goldenStepHash{Step:120, Hash:"92fcbdce2193c435"}, box2d_test.goldenStepHash{Step:150, Hash:"6f23c325670102c4"}, box2d_test.goldenStepHash{Step:180, Hash:"9c0ee5d707dd95a0"}, box2d_test.goldenStepHash{Step:210, Hash:"3edb9cd3cd902ae6"}, box2d_test.goldenStepHash{Step:240, Hash:"2e5763d0670a7bb3"}}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -3,3 +3,3 @@
        	            	   Step: (int) 30,
        	            	-  Hash: (string) (len=16) "b98fd6056df1b2af"
        	            	+  Hash: (string) (len=16) "a2be51d8d301ad55"
        	            	  },
        	            	@@ -7,3 +7,3 @@
        	            	   Step: (int) 60,
        	            	-  Hash: (string) (len=16) "f9dddf09b7acf106"
        	            	+  Hash: (string) (len=16) "04d685515a3d5ecb"
        	            	  },
        	            	@@ -11,3 +11,3 @@
        	            	   Step: (int) 90,
        	            	-  Hash: (string) (len=16) "1641c1b8a5940574"
        	            	+  Hash: (string) (len=16) "41f6f426dbd03462"
        	            	  },
        	            	@@ -15,3 +15,3 @@
        	            	   Step: (int) 120,
        	            	-  Hash: (string) (len=16) "d0fa87101552af9b"
        	            	+  Hash: (string) (len=16) "92fcbdce2193c435"
        	            	  },
        	            	@@ -19,3 +19,3 @@
        	            	   Step: (int) 150,
        	            	-  Hash: (string) (len=16) "4b8f31adcb69d63f"
        	            	+  Hash: (string) (len=16) "6f23c325670102c4"
        	            	  },
        	            	@@ -23,3 +23,3 @@
        	            	   Step: (int) 180,
        	            	-  Hash: (string) (len=16) "79b7abfcb65885e1"
        	            	+  Hash: (string) (len=16) "9c0ee5d707dd95a0"
        	            	  },
        	            	@@ -27,3 +27,3 @@
        	            	   Step: (int) 210,
        	            	-  Hash: (string) (len=16) "483091497c152cde"
        	            	+  Hash: (string) (len=16) "3edb9cd3cd902ae6"
        	            	  },
        	            	@@ -31,3 +31,3 @@
        	            	   Step: (int) 240,
        	            	-  Hash: (string) (len=16) "1b6f00e55f044391"
        	            	+  Hash: (string) (len=16) "2e5763d0670a7bb3"
        	            	  }
        	Test:       	TestGoldenStep
        	Messages:   	scene mixed_rain step hashes differ — solver determinism broken
--- FAIL: TestGoldenStep (0.08s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@smsunarto

Copy link
Copy Markdown
Member Author

The 16 failures were the amd64 golden tests: golden values are partly computed by test code (scene layout, seeded input tables, the spring integration loop), and go build never compiles test files, so the no-FMA gate could not see those fusion sites. Rounded those products and extended the gate to compile the test binary too — arm64 now emits zero FMA instructions in both the package and the test binary.

🤖 Addressed by Claude Code

@smsunarto
smsunarto force-pushed the scott/box2d-go-cardinal-ecs-ce311c branch from c778ac9 to 56070bd Compare August 4, 2026 03:33
Comment thread pkg/box2d/core.go Outdated
@smsunarto
smsunarto force-pushed the scott/box2d-go-cardinal-ecs-ce311c branch from 56070bd to dbf051e Compare August 4, 2026 05:00
Comment thread pkg/plugin/physics2d/internal/reconcile.go Outdated
Comment thread pkg/plugin/physics2d/internal/contact_flush.go Outdated
Comment thread pkg/box2d/core.go
Comment thread pkg/box2d/oracle_misc_internal_test.go Outdated
Comment thread pkg/box2d/world_step.go
smsunarto and others added 18 commits August 4, 2026 21:00
Port the full Box2D v3.2.0 feature set to pure Go at pkg/box2d: rigid
bodies, all 5 shape types, the TGS-soft solver with sub-stepping, all
7 joint types, sensors, continuous collision (bullets), and world
queries/explosions/debug draw. Float64 throughout, with package-wide
FMA-safe arithmetic and ported deterministic trig so results are
bit-identical across architectures (amd64/arm64), verified by golden
hash tests gated in CI.

Migrate pkg/plugin/physics2d off its CGO backend onto pkg/box2d:
- Kill the package-global runtime singleton; Plugin now owns a
  per-instance Runtime, enabling parallel tests and multiple physics
  worlds per process.
- Replace the WorldID() uint32 CGO escape hatch with a typed
  Engine() *box2d.World accessor.
- Delete internal/cbridge and the vendored Box2D C source
  (third_party/box2d); the plugin builds with CGO_ENABLED=0.
- Validate against CGO-recorded golden traces: contact/sensor event
  sequences and query results match exactly, body state matches
  within tolerance for the float64 vs float32 backend difference.

Also fixes a latent bug this port's fuzzer found in the ported
solver: a body leaving the awake set via SetBodyType/DisableBody kept
a stale body-move-event index, causing an out-of-bounds read on a
later forced sleep (upstream C has the same gap, unguarded).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move the cross-arch (amd64/arm64) golden-trace determinism job out of
common-go-ci.yaml into a dedicated box2d-determinism.yaml workflow,
scoped to pkg/box2d changes instead of every Go change repo-wide. Also
run the deterministic op-sequence fuzz corpus (added by pkg/box2d's
hardening tests) alongside the golden traces, and give the check its
own independently visible status on PRs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Temporary. TestGoldenDistance/Joint/Step/Continuous/Math all failed
cross-arch on the CI amd64 runner while arm64 matched the committed
golden (generated on arm64). This logs per-iteration bit patterns for
the failing vector_pipeline_chain golden case so the divergence point
can be bisected from a single CI run. Will be removed once found.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The golden bit-pattern tests were generated on arm64 and failed on the
CI amd64 runner. Root cause: the package's FMA rule only guarded
products written inline as an operand of + or -. Fusion is also legal
when an unrounded product reaches the + through a local, a struct
field, or a function parameter that is later inlined -- e.g.
MulSV's `s * v.X` flowing into mulAdd's addend, which arm64 emitted as
FMADDD and baseline amd64 did not. Disassembly showed 161 such sites.

Round products where they are formed, and additionally round the
operands the math_fma.go helpers receive so a caller's product is
rounded once the call is inlined (the smaller fix at most sites).
arm64 now emits zero FMA instructions, and the regenerated goldens
carry exactly the values amd64 was already producing.

Add TestNoFusedMultiplyAdd, which compiles the package for arm64 and
amd64/v3 and fails on any FMA instruction in the assembly. Reviewing
source for this is unreliable, so the invariant is now machine-checked
and wired into the determinism workflow ahead of the golden tests, so
a regression reports its cause rather than just "bits differ".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeQL flagged the workflow for relying on the default GITHUB_TOKEN
scope. The job only checks out and builds, so contents: read is
sufficient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit removed fusion from the package but the amd64
golden tests still failed. Golden values are partly computed by test
code -- scene layout, seeded input tables, the spring integration loop
-- and `go build` never compiles test files, so the gate could not see
those sites. Three goldens diverged for exactly that reason.

Round the products in those computations, including one that is only a
product after the compiler strength-reduces a division by a power of
two into a multiply. Compile the test binary in the gate as well, so a
fusion introduced by a future test is caught the same way.

arm64 now emits no FMA instruction in either the package or the test
binary, and goldens verify clean on repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The review job has failed on every run since it was added, always with
total_cost_usd 0 and duration_ms around 100 -- the first model call is
rejected before any tokens are billed, so retrying cannot help. The
action hides the API error by default, which leaves no way to tell a
401 from a quota or malformed-request failure.

Turn on show_full_output so the next run prints the real message.
Revert once the cause is known.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The workflow only had a path-filtered `push` trigger, so the status
landed on whichever commit happened to touch pkg/box2d and never on the
pull request head itself. A later commit outside those paths left the
PR with no determinism result at all -- the state this very PR was in --
and pull requests from forks never ran it.

Add the same path-filtered `pull_request` trigger. The path list is
duplicated rather than shared through a YAML anchor because the Actions
workflow parser does not support anchors.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Destroying a body, rebuilding it, or replacing its fixtures ends every
contact it was part of, but no End event reached consumers. The engine
does report end-touch for a destroyed body; by the time those records
are drained the shapes no longer exist, so they cannot be resolved to
entities and are dropped. Meanwhile the reconciler had already removed
the pair from ActiveContacts, so the rebuild diff could not report it
either. A consumer that latches state on Begin -- an "is grounded" flag,
say -- stayed latched forever.

Synthesize the End from the persisted pair metadata as it is pruned and
hold it until the next flush, sorted so the sequence stays
deterministic. The existing destroy test only asserted the tick did not
crash, which is how this went unnoticed; the new test asserts the End
actually arrives and fails without this change.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dropping upstream's global world registry left every World sharing one
id namespace, so equivalent allocation slots produced identical ids.
worldB.IsBodyValid(idFromWorldA) returned true and DestroyBody with a
foreign id silently destroyed an unrelated body in world B -- confirmed
by reverting the fix, where the new test panics inside BodyPosition
because the body really was gone.

Give each World a distinct owner token from a process-wide counter,
stamp it into the ids it issues, and check it in the validators, the
internal lookups and the destructive entry points. The token is checked
at runtime, not only under assertions, since a foreign id's index and
generation routinely collide with a live object. Tokens wrap at the
uint16 range; a reused token only weakens detection and never accepts
an id that fails the index, generation and liveness checks.

The token never reaches the solver or any golden. Cross-world tests
that compared whole ids now compare the slot index and generation and
additionally assert that world0 is the owning world's token, so they
check more than before, not less.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The package exposes SetLengthUnitsPerMeter, but the tolerances upstream
derives from it were frozen Go constants, so a caller working in
non-meter units silently got upstream-divergent collision, continuous
collision and broad-phase behavior. Upstream evaluates them as macros
at use time.

Convert the length-scaled quantities -- LinearSlop, Huge,
SpeculativeDistance, ContactRecycleDistance and MaxAABBMargin -- to
variables recomputed by the setter, and leave the dimensionless
constants alone. Each default is derived from the same untyped constant
expression as before and only multiplied by a length unit of 1.0, which
is exact, so the defaults stay bit-identical; a test pins that with
Float64bits and the goldens are untouched.

Because LinearSlop is no longer constant-foldable, several expressions
that the compiler used to evaluate at compile time became runtime
products that could fuse into an FMA. TestNoFusedMultiplyAdd caught
them and they are rounded at their definitions.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Internal invariants are asserted through assert(), which compiles out
because debugAsserts is false. The checks that a definition came from
its Default*Def constructor rode along with them, so
CreateBody(&BodyDef{}) silently produced a body whose Rotation is
Rot{0,0} -- not a normalized rotation -- and the damage surfaced later
as garbage motion or a confusing panic far from the cause.

Split the two tiers: internal invariants stay compiled out, while the
13 public creation entry points always check their definition and
panic with a message naming the constructor to use. Also always
validate the handful of values where a bad input corrupts quietly
rather than failing loudly: a body's rotation must be normalized, and
a shape's density, friction and restitution must be finite.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scoping the gosec exclusion to G115 (see the lint change) surfaced 27
G602 slice-bounds diagnostics that a blanket exclusion had hidden.
Triaging them found five reachable from the public API: Polygon.Count
and SimplexCache.Count are exported fields, so a hand-built
Polygon{Count: 20} or SimplexCache{Count: 9} indexes past the
fixed-size arrays the port inherits from upstream.

Validate both at the entry points that let such a value into the
engine -- CreatePolygonShape, SetShapePolygon, ComputePolygonMass,
CollideChainSegmentAndPolygon and ShapeDistance -- so the failure is a
clear message at the boundary instead of a bounds panic deep in the
solver. The polygon lower bound is 1 rather than 3 because the port
itself routes 1- and 2-vertex polygons from capsule collision through
these paths.

The remaining sites are bounded by construction; each carries a
nolint naming the invariant, since G602 is intraprocedural and does
not see a guard even in the same function. One site, the tree rebuild
stack, is documented as not provably bounded -- upstream guards it
with an assert alone and the only alternative is swapping one panic
for another.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Excluding all of gosec for pkg/box2d silenced the 161 expected G115
integer-width conversions from the ported id packing and hashing, but
it also hid 16 G602 slice-bounds diagnostics -- five of which turned
out to be reachable from the public API. Silencing a whole linter to
quiet one rule is how those stayed invisible.

Exclude only G115 and leave the rest of gosec enforced there.

Reported by an external review of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The system factories were exported but uncallable: both take an
*internal.Runtime, which Go's internal-package rule puts out of reach
of every consumer outside pkg/plugin/physics2d. They had replaced
directly-registerable exported systems. Nothing outside the plugin
referenced them, so move the package to internal/system where its
scope is visible from its location; RegisterPlugin remains the
supported entry point.

Engine() promised access to "any Box2D feature not directly exposed"
while its own caveats said mutation desynchronizes the ECS and that
engine-created objects are lost on any rebuild. Document it as what it
actually is -- a read-only escape hatch for queries and inspection --
and make that usable by adding BodyID and ShapeIDs lookups from an
entity, so callers can reach the engine objects the reconciler owns
without guessing. Changes that must persist still go through the ECS
components.

Also return a non-nil empty slice from an AABB overlap miss: a nil
slice marshals as null where the previous backend produced [], and a
query result crosses process boundaries, so that is part of the
contract rather than an implementation detail.

Reported by external review and differential testing of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The G602 triage flagged two more reachable panics in the same class as
the ones already fixed but left them out of scope, which was the wrong
call: they are the same bug, in the same file, and cheap to close.

ShapeProxy.Count and SimplexCache.IndexA/IndexB are exported fields, so
a hand-built proxy can claim more points than its fixed-size array
holds, and a stale cache reused against smaller shapes can name points
that no longer exist. Both are then copied or subscripted directly.
Validate them at ShapeDistance, and validate proxy counts at ShapeCast
and TimeOfImpact, which take proxies the same way.

Removing the checks again makes the new tests fail with out-of-range
panics, so these were reachable crashes rather than theoretical ones.
The supported warm-start path -- feeding back the cache from the
previous call -- is covered and unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The oracle-test sweep found that src/mover.c was never ported:
CastMover and CollideMover gather collision planes, but the solver
that consumes them -- b2SolvePlanes and b2ClipVector, the core of the
character mover documented in docs/character.md -- had no Go
counterpart, along with the b2CollisionPlane and b2PlaneSolverResult
types. The earlier "0 functions missing" API audit scanned box2d.h
only; these two live in collision.h.

Port the file (72 lines of C) with the package's FMA discipline, and
add oracle tests whose expectations are hand-derived from mover.c and
character.md: single-wall pushout to -LinearSlop, push-limit clamping
with exact convergence iteration counts, corner resolution, the
push-reset-on-entry contract, all four ClipVector skip/clip branches,
and an end-to-end CollideMover -> SolvePlanes -> ClipVector pipeline
against a real world. One of those hand-derivations was initially
wrong and the code was right, which is the point of deriving
expectations from the C rather than from the port.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
smsunarto and others added 17 commits August 4, 2026 21:00
Raise package coverage from 88.7% to 96.9% with ~1,100 new assertions
whose expected values come exclusively from the vendored C (extracted
from git history -- upstream has no v3.2.0 tag), upstream's own
test/*.c suite, and docs/, never from running this port. Each
nontrivial expectation cites its C file and line. Where our float64
diverges numerically from C float32, tolerances are documented rather
than constants adjusted.

Every upstream unit test that applies is ported: test_math, test_id,
test_bitset, test_table, test_collision, test_distance, test_shape,
test_dynamic_tree, test_world, plus contracts from simulation.md and
character.md. The foundations agent compiled and RAN the vendored C
to produce byte-exact expectations for hashes, slot layouts and block
words.

Behavioral divergences from the C: none found. Two deliberate
constant deviations are now documented at their definitions instead
of being accidental: Pi keeps the decimal literal (C's float32 literal
actually evaluates ~8.7e-8 higher, so angle wrapping differs ~1.75e-7
per turn), and getIDBytes reports Go's real 64-bit int footprint.
C-faithful quirks pinned by tests: joint setters never wake bodies in
v3.2, boundingPowerOf2(x<=1) == 1, minFloat NaN asymmetry, and an
upstream out-of-range hazard in chain surface materials on open
chains (inherited faithfully, avoided by the tests).

Remaining uncovered code is almost entirely compiled-out debug
validation and defensive branches that are dead in the C as well.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ecks

Profiling against the old CGO backend found circle sweeps 62-86%
slower (3 heap allocations per call from any-boxed callback contexts),
~1.2 MB allocated per 5000-body step, and World.solve at 75% of engine
CPU. Three structural change sets, none touching a single float
expression -- byte-identical goldens after every batch are the proof:

- Query paths: callback contexts, sub-inputs and plane results move to
  per-World/per-tree scratch (saved and restored, so re-entrant queries
  from user callbacks stay correct). Raycast and circle sweep are now
  0 B/op, 0 allocs/op; 13-29% faster in the wrapper. Scratch fields sit
  at the end of their structs -- placing one mid-struct pushed hot
  scalars onto extra cache lines and cost 12% on overlap queries.

- Step scratch: broadphase move results/pairs return to World-owned
  buffers (upstream keeps them on b2BroadPhase; the port had made them
  per-call locals), and all arena slots grow geometrically and never
  shrink, matching upstream arena high-water behavior. Bytes per step
  drop 67-96%; wall clock is unchanged within noise, the win is GC
  pressure on any process sharing the heap.

- Hot loops: reslice-to-length in the contact solver color routines,
  manifold SAT loops and simplex handling so the compiler drops
  per-iteration bounds checks (verified zero remaining with
  ssa/check_bce), pointer-held sweeps instead of 80-byte copies in the
  TOI separation function, caller-provided simplex in ShapeDistance.
  ShapeDistance -25%, TimeOfImpact -12 to -19%, mid-size steps -6%.

The two behavioral clarifications are documented in code: worlds and
trees are single-threaded for queries as well as stepping, and the
*PlaneResult passed to PlaneResultFcn is valid only during the
callback, exactly as in the C.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restore upstream's task-system parallelism as an internal goroutine pool
with a hard guarantee upstream does not make: simulation results are
byte-identical for every WorkerCount, verified against the unchanged
golden files.

Engine (WorldDef.WorkerCount, 0 = serial, clamped to MaxWorkers and
GOMAXPROCS; serial worlds run today's exact code behind pool == nil):

- worker_pool.go: persistent workerCount-1 goroutines, channel dispatch +
  WaitGroup barrier, Step goroutine is worker 0. Static contiguous
  ascending ranges by ceiling division - no work stealing, so
  ascending-worker merges reproduce serial item order exactly.
- Per-worker taskContexts (the upstream b2TaskContext array this port had
  collapsed to one): bitsets merged by inPlaceUnion in worker order
  (contact state, joint events, enlarged sims, awake islands, sensor
  bits); sensorHits and bulletBodies concatenated in worker order;
  split-island candidate reduced with upstream's strict-> in worker order.
- Parallel stages: broadphase pair finding (per-worker pair arenas,
  worker-local head indices, serial createContact loop unchanged in
  moveArray order), narrow-phase collide, integrate velocities/positions,
  per-color constraint stages (one barrier per color; overflow always
  serial on worker 0 in upstream's stage position), finalize bodies,
  sensors.
- Deliberately serial: bullet loops (bullets query trees other bullets
  mutate - upstream tolerates that race, the -race gate cannot), island
  split, tree rebuild, contact creation, all event assembly, arena and id
  pools. Counters().TaskCount stays stage-counted, identical for every
  worker count.

Wrapper: physics2d Config.Workers (clamped, documented as a pure
throughput knob - determinism is unaffected).

Gates: golden suites re-asserted at workers 2/4/8 against the same
testdata; fuzz corpus replayed at w=1 vs w=4 with checkpoint hash
equality; TaskCount parity test; -race over goldens plus a stress scene
sized to push every dispatch past its grain (also added to determinism
CI); no-FMA disassembly gate; golangci-lint clean.

Bench (Apple M5 Max, darwin/arm64): MixedRain 5000 bodies 2.68ms -> 1.26ms
per step at workers=18 (2.1x); 1000 bodies 1.2x; small scenes stay on the
inline serial path below grain thresholds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 41-agent adversarial review of the multithreading commit confirmed no
simulation-correctness defect but found the verification gates weaker
than claimed and small scenes slower with workers enabled. Fixes:

Gate integrity:
- Remove the GOMAXPROCS clamp on WorkerCount. It silently degraded every
  worker-matrix test and CI row to GOMAXPROCS-way partitions (vacuously
  green on small runners: w=8 rows ran 4-way partitions on 4-vCPU CI,
  serial-vs-serial on 1 CPU). Worker count is now the caller's explicit
  choice, as upstream; oversubscription costs throughput, never
  correctness.
- Pool-engagement canary test (WorkerCount silently ignored can no
  longer stay green), exhaustive workerRange/forRangeWorkers property
  tests, and a real fan-out probe.
- Fuzz lockstep worlds now start from a base scene sized past every
  grain threshold, and the 25-seed op-churn table replays serial vs
  workers=4 - previously neither ever dispatched to the pool.
- Stress signature now folds full event-stream contents, and the stress
  scene runs a second pass with preSolve/custom-filter/mixing callbacks
  installed, exercising the concurrent-callback contract under -race.
- Wrapper coverage: golden traces replayed at Config.Workers=4, clamp
  tests for negative/oversized Workers.

Performance:
- Capped worker engagement: forRangeWorkers = clamp(n/grain, 1, workers)
  replaces the all-or-nothing split; grain now means minimum items per
  worker (upstream minRange semantics). MixedRain at Workers=18: 500
  bodies +26% slower -> 1.4x faster, 1000 bodies +18% slower -> 1.6x
  faster, 5000 bodies 1.86x -> 2.3x.
- Per-stage presize/merge loops bound by the same pure function as
  their dispatch (10-body world at w=64: +27%/step -> +2.6%).
- stepContext and dispatch capture vars moved onto World: the serial
  path is back to 0 allocs/op (the dispatch closures had forced a heap
  escape even with the pool disabled).

Robustness:
- runtime.Goexit inside a dispatched callback (e.g. t.Fatal) previously
  killed a worker silently and deadlocked a later Step; the pool now
  raises a sentinel panic and respawns the worker. Regression-tested.
- Worker panics re-raise the original panic value (type preserved,
  consistent with the inline path); byte-identical determinism means
  any panic reproduces at WorkerCount=1 with a full stack.
- Goroutine-leak and worker-panic paths now have regression tests.

All golden suites pass unchanged at workers 1/2/4/8; testdata untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… end

PGO: commit default.pgo (merged CPU profile from the step benchmarks:
mixed rain 1000/5000 bodies at workers 1 and NumCPU, pyramid and jointed
at workers 1). Measured with benchstat (n=6): MixedRain 5000 -9.8%
serial / -6.4% at 18 workers, Jointed -10.2%, MixedRain 500 -5.8%,
geomean -4.4%, no significant regressions, allocs unchanged.

Determinism is unaffected and now mechanically proven under PGO:
- the full golden + worker-matrix suites pass byte-identical when built
  with -pgo=default.pgo;
- TestNoFusedMultiplyAdd repeats its disassembly scan with the profile
  applied on arm64 and amd64/v3 (PGO steers inlining, and inlining is
  what exposes new fusion sites; the float64() roundings are semantic
  and survive it) — and now FAILS LOUDLY if the committed profile goes
  missing instead of silently dropping that coverage;
- the determinism CI golden step builds with the profile.
Go does not auto-apply a library's default.pgo: consumers put a profile
in their own main package (doc.go explains).

SoA: a full structure-of-arrays repack of the ~320-byte contact
constraint (mirroring upstream b2ContactConstraintSIMD's grouping,
arithmetic and iteration order untouched, goldens byte-identical) was
implemented, benchmarked, and REVERTED: it measured 3-15% slower across
every scene on arm64. Root cause, not noise: the solve loop went from
one constraint pointer to ~24 live slice headers (381 -> 629
instructions, register spills 34 -> 90) and the AoS record is exactly
5 cache lines that sequential prefetch already streams perfectly.
The negative result is recorded in the contact_solver.go header so the
experiment is not blindly repeated; SoA returns only with real SIMD
lane kernels that require it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracts the falling-circles step scene into stepBenchScene and adds a
Workers=NumCPU variant at 1000/5000 bodies, used for the CGO-vs-pure-Go
comparison in the PR description. Results are byte-identical at every
worker count; this measures throughput only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
solveContinuous declared its continuousContext as a local and passed its
address through DynamicTree.Query's `any` context parameter, forcing one
heap allocation per fast body per step (~80% of all steady-state step
allocations: 545 -> 13 allocs/op and 209 KiB -> 26 KiB per step on
MixedRain 5000 Workers_1). The context now lives at the tail of the
per-worker taskContext and every field a sweep consumes is reinitialized
at the top of solveContinuous, so results are identical by construction:
no float arithmetic, ordering, or id assignment changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Narrow TreeNode.Child1/Child2/Parent to upstream's int32 width so the
node is exactly 64 bytes (one cache line; two nodes per Apple M-series
128-byte fetch) instead of straddling two lines. Indices stay Go int in
every signature and local - the narrow width is storage only, converts
losslessly, and no index approaches 2^31, so results are byte-identical
(all goldens pass unchanged at every worker count).

The dynamic tree is the write-and-read-hot structure in proxy-churn
scenes (insert/remove/balance/refit/rebuild/query are all node-bound),
so this single layout change is the largest serial win of the perf
effort. Same-hour benchstat A/B (n=6, Apple M5 Max):

  MixedRain  100 bodies  serial -53%  workers -41%
  MixedRain  500 bodies  serial -30%  workers -13%
  MixedRain 1000 bodies  serial -31%  workers -15%
  MixedRain 5000 bodies  serial -29%  workers -13%
  Jointed                        -5%
  Pyramid (static tree)          unchanged - the expected signature
  Raycast/Overlap 5000-body queries improve ~10-20% as well.

Implemented during the overnight experiment run (E3); verified and
adjudicated against the keep rule (p=0.002 on every improving row, no
regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gather

Cut wrapper-side per-tick allocations (-32% B/op at 1000 bodies, -36% at
5000) by reusing buffers that were re-allocated every tick:

- gatherRebuildEntries appends into a caller-owned scratch slice reused
  across ticks (was: fresh cap-64 slice regrown to N every tick)
- ReconcileFromECS sorts into rt.reconcileSortScratch (was slices.Clone)
- destroyOrphanBodies uses index-based binary search over the
  EntityID-sorted entries (was: per-tick map[EntityID]struct{})
- gatherLiveContacts reuses its result map, body-id slice, and
  ContactData buffer via runtime scratch fields (runs up to twice/tick)
- writeback entries gathered into a closure-owned scratch slice

Behavior-preserving: iteration orders, written component values, and
emitted event sequences unchanged; golden traces pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stages

The six per-color contact-solver stages shared one dispatch grain
(solverColorGrain = 32), but only solve and relax do enough work per item
to cover a barrier. Wall-clock stage instrumentation (MixedRain 5000
bodies, 600 steps, Apple M5 Max, 18 workers) showed the other four losing
to serial at that grain — warm start 0.1444 -> 0.2387 ms, prepare
contacts 0.0603 -> 0.0728, restitution 0.0339 -> 0.0575, store impulses
0.0368 -> 0.0540 — because each per-color dispatch pays a ~6-10 us
barrier that exceeds the stage's own work.

Give those stages (plus prepare joints) their own grain,
solverLightColorGrain = 1024, so forRangeWorkers keeps them on the
dispatching goroutine until a color is genuinely large. Solve and relax
stay on solverColorGrain = 32, where parallel wins 1.56x.

The value was swept end to end over 32/64/128/256/512/1024/2048/1<<30,
runs interleaved across candidates (a sequential sweep let build order
masquerade as an effect), 10 reps of -benchtime=50x each at Workers_18.
Every candidate >= 64 gains ~5.5% on StepPyramid; MixedRain 1000 and 5000
are flat everywhere; a denser pyramid (colors of 322-409 items) separates
them, with 64 gaining only 8.4% where 256/1024/1<<30 gain 12-14% — a
grain of 64 wins the 20-row pyramid only because its colors happen to
fall under 2*64. Among the tied candidates, 1024 matches the ~3.7 ns per
item cost of these stages against the barrier and still lets colors of
2048+ items dispatch.

Determinism is unaffected: grain changes only how many workers engage,
and per-color partitions are byte-identical by the graph coloring. None
of the retuned stages write per-worker state, so the presize/dispatch/
merge invariant is untouched — the only per-color stage with a per-worker
output is solve/relax (jointStateBitSet), and maxColorWorkers still
derives its bound from the same (itemCount, solverColorGrain) pair.

Gates: go build ./...; go test ./pkg/box2d/; go test -race -run
'TestGolden(Step|Continuous)|TestWorkerStress|TestPool' ./pkg/box2d/; go
test ./pkg/plugin/physics2d/...; go test -run TestNoFusedMultiplyAdd
./pkg/box2d/; golangci-lint run ./pkg/box2d/... — all pass, goldens
unchanged.

benchstat n=6 -benchtime=50x, PRE vs POST, interleaved:
  StepPyramid/Workers_18            278.4µ -> 263.6µ  -5.30% (p=0.041)
  every other row                   ~ (no significant change)
  allocs/op and B/op                unchanged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eep events

Two behavior divergences from upstream v3.2.0 found by the C-parity audit:

- PrismaticJointSpeed panicked when called from a step-time callback
  (locked world). Upstream b2PrismaticJoint_GetSpeed is the one joint
  accessor that deliberately bypasses the world-lock early-out
  (b2GetWorld + b2GetJointSim) so it stays readable during callbacks;
  the port routed it through getJointSimCheckType, which returns nil
  under lock. Resolve the joint without the lock check, matching C.

- transferBody blanket-invalidated bodyMoveIndex whenever a body left
  the awake set, suppressing legitimate FellAsleep notifications after
  a SetBodyType or DisableBody round trip within one post-step window.
  Upstream b2TransferBody does not touch bodyMoveIndex. Restore parity
  at the source and make the trySleepIsland consumer stale-safe instead
  (bounds + body-identity check before stamping FellAsleep), which also
  guards the out-of-bounds read upstream is exposed to.

Also document that WorldDef.EnableContactSoftening is inert in this
port: upstream reads it only in the SIMD prepare path, which the
scalar-only port does not transliterate.

Both fixes carry regression tests proven to fail without the change.
Golden determinism files are byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NaN-poison testing proved the solver never reads a stale field (prepare
initializes everything it reads for points < pointCount), but removing
the clear showed no win above the noise floor in interleaved min-of-6
A/B runs, so the defensive clear stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/code-review of the last five commits found no shipped correctness bug,
but three checks had quietly stopped checking.

The cross-architecture golden step was changed in place to -pgo, so the
default no-profile build lost golden coverage on amd64 and arm64. Split
it back into two steps; the PGO run stays as the second one.

The light per-color stages moved to a grain of 1024, which put their
multi-worker path out of reach of every scene in the suite: prepare
joints, prepare contacts, warm start, apply restitution and store
impulses now ran inline everywhere, including under -race. Add a scene
that packs 2200 joints and 2200 contacts into ONE color, which works
because a static-vs-dynamic constraint claims only the dynamic body's
bit, so thousands of boxes on one ground and thousands of bobs on
static anchors all pack together. TestWorkerStressLightStageColorMatches
Serial is the byte-identity gate (its name matches the CI -race pattern
already), and an internal test asserts the premise against the
constraint graph so the suite cannot silently fall back to the inline
path. Cost: the -race step goes 6.5s -> 8.2s. Growing the existing
stress scene instead would have cost about a minute.

Three solver comments still claimed presize == dispatch == merge after
the grain split. The bound is merely >= now, and it holds only while
the light grain stays above the heavy one, so state that and make it a
compile error rather than a comment.

Also: rename TestBodyMoveIndexInvalidatedOnAwakeSetExit, which named a
mechanism the shipped fix deliberately does not use; rewrite the E14a
fuzz note as fixed; record the locked-world accessor exemption on
getJointSimCheckType, where it is enforced; correct the dynamic_tree
header, which claimed int in every local while five locals are int32;
and drop -a from the PGO no-FMA builds (3.62s -> 2.89s measured).

Goldens are byte-identical: testdata is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…CS memory

Second half of the post-review remediation. Every change is
byte-identical: the physics2d golden trace and the box2d goldens pass
unmodified.

gatherLiveContacts sorted the tracked body ids on every call to buy a
determinism the result never needed. World.BodyContactData fills
ShapeIDA/ShapeIDB and the manifold from the contact record, not from the
endpoint that asked, so both endpoints of a pair produce a
byte-identical ContactPairInfo and first-wins dedupe cannot depend on
iteration order; every consumer is order-free too. Drop the sort, the id
gather and the liveIDsScratch field, and iterate rt.Bodies directly.

Box2D reports each contact twice, once per endpoint, and the old dedupe
ran after two shapeIdentity resolutions had already spent six world
lookups on the copy that was about to be discarded. Stamp contact
indices in a generation-stamped []uint32 and skip before resolving
identities. The map check stays as a backstop, so a stamp bug can only
cost work, never drop a pair.

  BenchmarkStep, Apple M5 Max, n=6, benchstat vs the previous commit:
    Bodies_100   -3.5%   (p=0.002)
    Bodies_500   -4.0%   (p=0.002)
    Bodies_1000  -5.4%   (p=0.002)
    Bodies_5000  ~       (p=0.937)
    geomean      -3.3%           allocations flat

gatherLiveContacts also returned a map aliasing runtime scratch under a
comment-only contract. It takes a destination now, which lets the
adopt path gather straight into ActiveContacts and removes the round
trip through a second map.

Separately, the pipeline system kept its two per-tick gather buffers in
a closure and truncated them with a bare [:0]. WritebackEntry holds
cardinal.Refs and PhysicsRebuildEntry holds each collider's shape
slice, so everything past the new length went on pinning archetype
storage for entities that no longer existed, out of Reset()'s reach.
Move both onto the Runtime with the rest of the per-tick scratch, clear
the tail after each gather, and state the rule on the struct. The init
path shares the buffer now, which also restores the pre-size it lost.

Also drops a gosec suppression in the reconcile binary search by
computing the midpoint as lo+(hi-lo)/2, and replaces the benchWorkers
global with a threaded parameter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… pin

Address three PR review threads:

Assert net (dead code in every build): debugAsserts now lives behind a
build-tag pair (core_asserts_off.go / core_asserts_on.go) and the
determinism workflow runs the full suite with -tags box2d_asserts on both
architectures, so the ~hundreds of internal invariant checks finally
execute somewhere. Tests that pin release-build guard semantics
(B2_ASSERT-then-ignore in C) skip or fork under the tag via a
buildWithAsserts mirror; foreign-id destroys and the degenerate hull
gained tagged twins pinning the panic tier. Wiring the net in exposed
three latent issues, all fixed here:
- destroyArena asserted allocation == 0, which upstream
  b2DestroyArenaAllocator does not; it fired on the supported
  Destroy-after-callback-panic path.
- the RotationBetweenUnitVectors oracle's float64 loop accumulator
  crosses zero at ~7.5e-16, so the x==0&&y==0 guard missed the singular
  point and that iteration compared zero against zero vacuously.
- TestOracleShapeHelpersDefaultArm treated every default arm as
  release-only when only two assert in C; the assert-free arms now stay
  covered in both builds.

Scratch retention: reconcileSortScratch was the one Runtime scratch
buffer still truncated with a bare [:0]; it now goes through
clearScratchTail so a shrinking scene cannot pin shape/vertex memory of
destroyed entities.

Contact stamp direction: the map-check comment claimed a stamp bug could
only cost work, but the check only covers stamp false negatives — a
false positive drops the pair before it. Rewrote the comment to state
which side each mechanism holds, documented PackContactID slot 0
(1-based dense index) as a contract, and added a slot-discriminating
assertion to TestOracleContactID_CReference: its round-trip checks were
invariant under a coordinated swap of the index1/generation slots, so
the layout the physics2d seen-stamp depends on was not actually pinned.

Golden determinism files are byte-identical; both build variants green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…build

Address two PR review threads on c4a4b2a:

assert(cond) evaluates its argument even though the check compiles out,
and the seven multi-line validators (IsValidVec2/AABB/Rotation/Transform/
Ray/Plane, ValidateHull) exceed the inline budget, so the release binary
kept real calls in per-step paths (finalizeBodiesTask per body,
MoveProxy/EnlargeProxy per moved proxy, every local raycast) that
upstream's `((void)0)` release macro never evaluates. Wrap those ~30
call sites in `if debugAsserts { ... }` so the build-tag const folds the
evaluation away, and document the per-site cost rule on assert and in
core_asserts_off.go. Release codegen now shows no validator calls
outside the always-on requireValidDefField tier (verified with
-gcflags=-S); golden files are byte-identical, both build variants,
no-FMA and race gates green.

Also restore both-tier coverage for the two asserting default arms in
TestOracleShapeHelpersDefaultArm: the box2d_asserts build now pins that
computeShapeAABB and makeShapeDistanceProxy panic (matching shape.c:650
and :1020) instead of skipping them, mirroring how every other tag fork
in c4a4b2a pins both tiers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A panic out of a user callback unwinds past Step's unlock, latching
w.locked forever: every later Step hit the silent reentrancy early-out
and the world froze with no error anywhere (or tripped a bare assert
under box2d_asserts). A server with panic-recovery middleware around its
tick would keep calling Step and silently stop simulating.

Recover-and-continue is not an option for this engine: a world resumed
from a half-integrated step would silently diverge from every
deterministic replica, and determinism is the product. So make the
poison model explicit and loud. A completion-flag defer (catches
runtime.Goexit too, and re-raises the original panic value untouched)
marks the world stepPanicked; the next Step panics in every build with a
message naming the cause and the remedy, checked before the reentrancy
guard so the latched lock cannot mask it. Destroy stays valid on a
poisoned world (the destroyArena side of this unwind path landed in
c4a4b2a).

The new regression test re-steps the SAME panicked world — the case
TestPanicInCallbackPropagatesOriginalValue (Destroy + fresh world) never
covered — on both the serial and pool unwind paths, and fails without
the fix. Golden files are byte-identical; both build variants, race and
wrapper gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@smsunarto
smsunarto force-pushed the scott/box2d-go-cardinal-ecs-ce311c branch from e18c7c0 to 34ec774 Compare August 5, 2026 04:05
Comment thread pkg/box2d/world_step.go Outdated
e18c7c0 made Step fail loudly on a poisoned world, but the latched
world lock also gates every query and event accessor, and those guards
return a zero value: a poisoned world answered raycasts with no hits and
overlaps with no shapes — a wrong answer, not a no-op. A recovering game
loop reads (line of sight, triggers, event drain) before it steps, so it
would act on "nothing is there" for a tick before the Step panic ever
fired — the same silent divergence the poison exists to prevent, one
call earlier.

Hoist the poison check into a shared panicIfPoisoned helper with one
message const, and call it first in Step, the seven world queries
(OverlapAABB/OverlapShape/CastRay/CastRayClosest/CastShape/CastMover/
CollideMover), the four event accessors, and Draw — before the
assert(!w.locked) reentrancy guards, so the box2d_asserts build reports
the real cause instead of a bare assertion failure. The genuinely
reentrant path (a query from inside a step callback) is unchanged:
stepPanicked is false there, and upstream's identical locked guards are
only reachable reentrantly because C's flag cannot latch.

The regression test now also drives CastRay, OverlapAABB, BodyEvents
and ContactEvents on the poisoned world and requires the explicit
message; the read-path half fails without this fix. Golden files are
byte-identical; both build variants, race and wrapper gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread pkg/box2d/world.go
35320c8 covered the world-level queries, event accessors and Draw, but
body.go and shape.go carry nine more read paths with the identical
latched-lock zero-value guard: BodyContactCapacity, BodyContactData,
ComputeBodyAABB, ChainSegmentCount, ChainSegments, ShapeContactCapacity,
ShapeContactData, ShapeSensorCapacity and ShapeSensorData. The
consequence was live in this repo: physics2d builds its contact gather
from BodyContactCapacity+BodyContactData, so a poisoned world gathered
an empty live set and FlushBufferedContacts emitted a spurious
ContactEnd for every persisted touching pair — a wrong answer written
into game state one tick before the Step panic. ComputeBodyAABB was
worse than a zero count: an empty AABB at the origin is a
plausible-looking value a caller culls with.

panicIfPoisoned is now the first statement of all nine, and its doc
states the contract line this closes on: Step and every read path fail
loudly on a poisoned world; the mutators deliberately keep their plain
locked-guard shape, because a recovered tick cannot complete without
hitting a read or Step, and threading the check through ~30 setters
buys no additional safety.

The regression test now also drives BodyContactCapacity,
BodyContactData and ComputeBodyAABB on the poisoned world and fails
without the fix. Golden files byte-identical; both build variants, race
and wrapper gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants