Skip to content

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

Open
smsunarto wants to merge 28 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 28 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, yes: the Go engine is 20–30% slower, because it uses double-precision math where the C build used single. But at the scene sizes where that gap is widest, both engines finish in roughly 1–13% of a 60 Hz frame budget — not something a player can see. And the C bridge was locked to one core forever, while the Go engine steps on all cores and stays byte-identical at every worker count. On an 18-core machine, full game tick:

Scene C (single-core) Go (with workers)
5,000 bodies 5.49 ms 5.32 ms
1,000 bodies 1.054 ms 1.135 ms (+7.8%)

Large-scene raycasts are 2x faster in Go (no per-hit border crossing), query performance overall is at parity, and opt-in profile-guided builds (the compiler optimizes using a recorded CPU profile) recover a further ~5–10% of engine speed.

"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 wrapper bench harness on both sides (pkg/plugin/physics2d/test, falling-circles scene), -benchtime=50x -count=6, medians via benchstat.

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

Bodies CGO Pure Go, serial Pure Go, Workers: 18
100 127.4µs 158.1µs (+24.1%) — (below dispatch grains)
500 506.2µs 653.5µs (+29.1%)
1000 1.054ms 1.286ms (+22.1%) 1.135ms (+7.8%)
5000 5.488ms 6.565ms (+19.6%) 5.322ms (−3.0%)

Serial float64 Go pays ~20-30% vs float32 C at the tick level; the deterministic worker pool closes that and beats CGO at 5000 bodies. Engine-only (no ECS overhead, no CGO counterpart existed at this layer): MixedRain 5000 bodies steps at 2.42ms serial / 1.05ms at 18 workers (2.3×), and the opt-in PGO profile takes a further −9.8% serial at the engine level.

Queries (per-op, dense grid scene)

Raycast at 5000 bodies: 1808ns → 924ns (−48.9%); CircleSweep at 100 bodies −50.9%; CircleSweep mid-sizes +17-32%; remaining rows are statistically noise on sub-µs micro-queries (high run-to-run variance). Geomean across all 16 query rows: +2.9% ≈ parity, with the big-scene raycast halved (no per-candidate CGO crossing).

CPU profiles, 5000-body tick

CGO (pprof top):

runtime.cgocall                          37.4% flat   ← entire C engine, opaque to Go tooling
  cbridge.GatherLiveContacts             32.1% cum    ← per-tick contact extraction across the boundary
  cbridge.Step (bridge_step_advance)      5.7% cum    ← the actual physics step

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

Pure Go:

box2d.(*World).Step                      21.5% cum    ← fully attributable:
  updateBroadPhasePairs                  11.2%          pair-find (tree queries 10.6% flat)
  solve                                   8.2%          TGS-soft solver
internal.FlushBufferedContacts            6.8% cum    ← direct memory reads replace the 32% gather

The 32%-of-tick contact-gather tax is gone (direct slice access), the step is transparent to pprof/PGO, and the engine additionally guarantees byte-identical results across architectures and worker counts (goldens + no-FMA gate in CI) — a property the float32 C build never had on heterogeneous hosts.

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>
@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Claude finished @smsunarto's task in 6m 2s —— View job


Code review

Reviewed the port with a focus on the determinism machinery, the worker pool, and the physics2d migration rather than line-by-line over 29k lines of mechanical transliteration. I verified locally on this runner (linux/amd64):

Check Result
go build ./pkg/box2d/... ./pkg/plugin/physics2d/...
go test ./pkg/box2d/ -run TestGolden -count=1 ✅ (the codecov failures above are from a superseded commit)
go test ./pkg/box2d/ -run TestNoFusedMultiplyAdd -count=1 ✅ 17.3s
CGO_ENABLED=0 go test ./pkg/plugin/physics2d/... -count=1
golangci-lint ⚠️ not installed on this runner — could not verify

Overall this is unusually disciplined work. The determinism argument is mechanically enforced rather than asserted, and the reasoning is written down where a future maintainer will actually hit it. Specifically worth calling out:

  • nofma_test.go asking the compiler instead of auditing expressions, and extending the scan to the test binary and PGO-shaped codegen — the two places a source review would have missed. The builds[:len(builds):len(builds)] full-slice expression to avoid aliasing while appending to the slice being ranged is a nice touch.
  • worker_pool.go: workerRange/forRangeWorkers as pure functions of (n, grain, workerCount, k) with the "dispatch bound == presize bound == merge bound" invariant restated at every stage (solver.go:687, broad_phase.go:459, solver.go:1005). I traced jointStateBitSet through presize (solver.go:758) → write (solver.go:271) → merge (solver.go:1042) and the maxColorWorkers bound holds, including the overflow-on-slot-0 floor and the prepareJoints dispatch that uses the smaller jointCount.
  • Refusing to parallelize the bullet loops and island split because "upstream tolerates that race, the -race gate cannot" — correct call.
  • The per-worker joint-event dedup deviation (solver.go:260-268) comes with an actual proof of why it can't change the emitted set, not a hand-wave.
  • Engine() documented as read-only with the precise list of what the reconciler will clobber, and BodyID/ShapeIDs carrying explicit "do not cache across ticks" lifetimes.

Three findings below, one of which I'd fix before merge.


1. Non-deterministic body destruction on the FullRebuildFromECS error path

pkg/plugin/physics2d/internal/rebuild.go:97

// On error: destroy all bodies created so far and leave clean state.
for id := range newKnown {
    rt.DestroyEntityBody(id)
}

newKnown is a map, so this destroys in Go's randomized iteration order. idPool is a LIFO free list (pkg/box2d/id_pool.go:25-35): freeID pushes and allocID pops the tail, so the order in which ids are returned determines which indices the next bodies get. Different order → different body/shape indices → different solver-set ordering → divergent simulation.

This isn't contained to the error path, because PhysicsPipelineSystem logs the error and keeps the world alive:

// pipeline.go:86
if err := rt.FullRebuildFromECS(rt.Gravity, entries); err != nil {
    state.Logger().Error().Err(err).Msg("physics2d: FullRebuildFromECS failed (nil world recovery)")
}
return

Failure scenario: two servers replay the same inputs; one entity has an invalid ColliderShape so CreateBodyWithCollider fails partway through the rebuild. Both log the error, both recover on a later tick, but their id pools were unwound in different orders, so every body created afterwards lands on different indices and the two worlds diverge — exactly the class of bug this PR exists to eliminate.

The same function already sorts for precisely this reason 40 lines above (rebuild.go:46-56, "sorted for deterministic destruction order"), and destroyOrphanBodies sorts its orphans at reconcile.go:79. This one spot is the outlier.

Simplest fix: sorted is already EntityID-ordered and creation walks it in order, so destroy the prefix in order instead of ranging the map:

for j := range i {                       // entities created before this failure
    rt.DestroyEntityBody(sorted[j].EntityID)
}

(requires the loop to be for i, e := range sorted).

Fix this →


2. gatherLiveContacts aliasing contract is documented but unenforced

pkg/plugin/physics2d/internal/contact_flush.go:143

The returned map now aliases rt.liveContactsScratch and is invalidated by the next call. All three current callers are safe — adoptLiveContactsWithoutEmit copies out immediately, and diffActiveContactsAfterRebuild / refreshActiveContactsFromLive sit on mutually exclusive branches of FlushBufferedContacts, so no two gathers are ever live at once. I checked; today this is correct.

But it's a footgun with no test behind it: a future caller that does before := rt.gatherLiveContacts(); ...; after := rt.gatherLiveContacts() gets two handles to the same map and a silently empty diff — no panic, no failing assertion, just wrong events. Given that the diff-after-rebuild path is exactly the shape where someone would want a before/after comparison, I'd make this structural rather than a comment: take the destination map as a parameter (func (rt *Runtime) gatherLiveContactsInto(dst map[...]...)) so the caller owns the buffer and an accidental second live gather is a compile-time-visible decision.

Same note, lower stakes, for rt.castScratch / rt.overlapScratch in internal/query.go — those already do explicit save/restore for nesting, which is the right pattern; gatherLiveContacts is the one that doesn't.


3. CI: path-filtered workflow + required status check

.github/workflows/box2d-determinism.yaml:19-31

The dual push/pull_request triggers with duplicated path lists are correct and the comment explains why. One operational caveat worth knowing before this lands: if Determinism (amd64) / Determinism (arm64) are added as required status checks in branch protection, PRs that don't touch pkg/box2d/** will never report them and will sit blocked forever — GitHub treats a skipped-by-path-filter job as pending, not success. The usual workaround is an always-running job with the same name that short-circuits, or keeping these non-required. Not a code defect, just the failure mode to avoid on the follow-up branch-protection change.


Smaller notes (non-blocking)

  • gochecknoglobals off for pkg/box2d (.golangci.yaml) is reasonable for a port, but it also means nothing flags future process-global mutable state. The one that exists today, lengthUnitsPerMeter (core.go:95), is documented thoroughly with the right warnings — but that caveat lives only in box2d. Since physics2d's package doc leads with "Multiple Plugin instances in one process simulate fully independently" (plugin.go:122), consider a one-line pointer there that the length unit is the one shared knob.
  • Closure scratch retains cardinal.Ref past len (internal/system/pipeline.go:77-78, contact_flush.go liveIDsScratch). entriesScratch[len:cap] keeps stale Ref values reachable, so peak entity count sets the floor on retained references. Bounded and harmless, just worth knowing it's a deliberate trade.
  • FullRebuildFromECS still slices.Clones (rebuild.go:36) while the reconcile path was converted to scratch reuse. Intentional given rebuild is rare — mentioning only so it doesn't read as an oversight later.

Nothing here blocks the architecture; finding #1 is the only one I'd want fixed before merge. The cross-arch CI job on this PR is the remaining unchecked box from your own test plan, and that's the one that matters most.
scott/box2d-go-cardinal-ecs-ce311c

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>
Comment thread .github/workflows/box2d-determinism.yaml Fixed
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>
@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 and others added 4 commits August 1, 2026 19:21
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>
The templates still carried wire.gen.go from before commit 7a20719,
which moved components, commands and events to a single Serializable
contract (UnmarshalWire returns (any, error), MarshalWire required)
and removed cardinal.RegisterCommandCodec. As a result
`go build ./pkg/...` failed and the repo Lint and Test jobs were red
regardless of the change under review.

Update the affected files to the current contract, following the
shapes in pkg/plugin/physics2d, and drop the codec registration
blocks. These are normally produced by `world sdk generate`, which
needs Docker; each file notes that it was hand-updated pending a
regeneration.

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>
@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 and others added 19 commits August 1, 2026 19:39
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>
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>
smsunarto and others added 2 commits August 2, 2026 05:46
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>
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