Skip to content

perf(space): skip PRE-listener matching without PreListeners; solve over a cached arbiter array - #257

Merged
NewKrok merged 6 commits into
masterfrom
claude/perf-bit-identical
Oct 8, 2026
Merged

NewKrok merged 6 commits into
masterfrom
claude/perf-bit-identical

Conversation

@NewKrok

@NewKrok NewKrok commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Stacked on #255 (it relies on its golden tests). Once #255 is merged, this targets master.

Summary

Two Space.step() optimisations that keep the simulation bit-identical:

  1. runPreListeners returns immediately while the space has no PreListener.
    • Before this change, every colliding arbiter built MRCA chains and did four CbSetPair lookups every step, only to find empty PRE lists. This affected every physics demo, since none of them registers a PreListener.
    • The count lives on ZPP_Space.preListenerCount and is maintained in ZPP_InteractionListener.addedToSpace / removedFromSpace. Those are the only entry and exit points; precedence and cbType changes and swapEvent all go through them.
  2. iterateVel / iteratePos walk an array of the active, mutable arbiters, in list order.
    • Before, each of the 10+10 iterations re-walked and re-filtered both linked lists.
    • The array is rebuilt at the start of each solver phase, since CCD runs between the two. It is also rebuilt after a constraint break, because con.broken() can wake bodies and re-list arbiters.

Measured: benchmarks/demos.mjs from #256, min of 2×7 trials, base = #255

Demo base (ms/step) this PR (ms/step) change
exploding-50 40.3 33.6 −17%
tracked-vehicle 0.279 0.244 −13%
plinko 18.3 16.2 −12%
asteroid-field 14.4 12.8 −11%
volcano 2.09 1.85 −11%
popcorn 1.06 0.98 −8%
pulse 20.1 19.1 −5%

The showpieces (≤0.1 ms/step) are mostly 2–14% faster; the few small positive deltas are within noise.

The state hash is identical to the base build for every demo except cloth, which reads performance.now() and is not reproducible even against itself.

Test plan

  • Full suite, including bit-exact Behaviour.golden + Determinism.golden (7701 + 79)
  • New tests/space/Space.preListenerGate.test.ts covers the count through add / remove / clear / re-add, precedence and cbType changes, mid-simulation add, other listener kinds, and two spaces
  • format, lint, typecheck, build

🤖 Generated with Claude Code

https://claude.ai/code/session_01JPF7yx9ag7c2pjXpVo4afv


Generated by Claude Code

claude added 5 commits October 8, 2026 06:05
…g after STATIC→DYNAMIC

- presteparb runs once per sub-step under the same stamp, so with
  subSteps > 1 an ended contact generated END once per sub-step; the second
  crashed on the freed callback set ("reading 'remove_arb'"). END is now
  generated at most once per stamp.
- An arbiter that already generated END could fall asleep looking live
  (body switched STATIC while touching a static body); waking it fired END
  again (same crash) and suppressed BEGIN on renewed contact. Ended arbiters
  now stay ended across the sleep shift.
- Pooled arbiters kept a stale endGenerated from a previous space / run;
  reset it when an arbiter is (re)initialised.
- transmitType re-syncs shapes with the broadphase so the AABB tree moves a
  body's leaf to the tree matching its new type; a body switched back to
  DYNAMIC on a static floor no longer sinks ~3 px for several steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPF7yx9ag7c2pjXpVo4afv
…L_HASH

Constraints attached to space.world were saved as "no body" and the
restored space threw "cannot be simulated null bodies" on its first step.
The world is now stored as body id -2 in both formats (older readers still
read it as null, as before). A SPATIAL_HASH space was saved as
SWEEP_AND_PRUNE because the spatial hash also sets is_sweep; it is now
stored as "SPATIAL_HASH" / broadphase byte 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPF7yx9ag7c2pjXpVo4afv
…r safety net)

Behaviour.golden: 35 scenarios, one per subsystem, each recording sampled
body states, the full callback log and probes (contacts, impulses, queries,
mass data). Three tiers: bit-exact vs golden (platform-pinned, CI
linux-x64), 1e-6 tolerance for non-chaotic scenarios on every platform
(guarded by a dt-perturbation test), and run-to-run identity.
PublicApi.surface: every runtime export and public class member of the
five entry points. regen-goldens workflow records both golden files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPF7yx9ag7c2pjXpVo4afv
…rialization fixes; mid-contact snapshot limitation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPF7yx9ag7c2pjXpVo4afv
…ver a cached arbiter array

Both keep the simulation bit-identical (behaviour + determinism goldens pass
unchanged):
- runPreListeners returns immediately while the space has no PreListener
  (counted in ZPP_InteractionListener added/removedFromSpace). Before, every
  colliding arbiter built MRCA chains and four CbSetPair lookups each step
  only to find empty PRE lists.
- iterateVel / iteratePos walk an array of the active, mutable arbiters in
  list order instead of re-walking and re-filtering both linked lists on
  every iteration. Rebuilt at the start of each solver phase and after a
  constraint break (which can wake bodies and re-list arbiters).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPF7yx9ag7c2pjXpVo4afv
@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@NewKrok
NewKrok merged commit f4df512 into master Oct 8, 2026
15 checks passed
@NewKrok
NewKrok deleted the claude/perf-bit-identical branch October 8, 2026 18:51
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