Skip to content

perf(examples): cut Bunnymark's update loop by a quarter - #15

Merged
frostney merged 1 commit into
mainfrom
perf/bunnymark-update-loop
Oct 1, 2026
Merged

frostney merged 1 commit into
mainfrom
perf/bunnymark-update-loop

Conversation

@frostney

@frostney frostney commented Oct 1, 2026

Copy link
Copy Markdown
Owner

At 10,000 sprites the instanced Bunnymark frame is bound by updateBunnies, not by rendering: about 27.6 ms of a 28.2 ms frame on the Radeon RX 6600M. This takes about a quarter off that loop with the same rendered frame and default syntax.

  • Module constants become locals. bunnyInstanceStride, bunnyXOffset and bunnyYOffset were read per sprite, and GocciaScript 0.14.0 compiles a module-level const read inside a function to OP_GET_GLOBAL.
  • Edge tests compare against bounds computed once per call, instead of adding half the texture size per sprite.

Together that is 88.76 opcodes per sprite down to 78.76. addBunnies is unchanged; it only runs when sprites are added.

Measurements

Ryzen 9 6900HX, Linux Mint 22.3, GocciaScript 0.14.0, 10,000 fixed-seed sprites, 100 frames per run, three interleaved runs. "Before" is tag 0.1.0, whose examples are identical to main.

Before After Change
Update loop alone, no window 25.64 ms/frame 19.09 ms/frame -25.5%
Whole frame, DrawMeshInstanced, llvmpipe under Xvfb 41.78 ms (23.9 FPS) 35.69 ms (28.0 FPS) +17.1% FPS
Whole frame, DrawTextureV, llvmpipe under Xvfb 106.82 ms (9.4 FPS) 100.67 ms (9.9 FPS) +6.1% FPS

Both draw paths lose about 6 ms per frame, which matches the saving in the loop alone. The per-run times and the opcode profile are in benchmarks/import-binding-aliases.md.

Not measured: the two Radeon renderers and the two Macs. On hardware the instanced frame is almost entirely this loop, so the gain there should be larger than under llvmpipe, but that is an expectation, not a result.

Equivalence

The graphical integration test still finds the direct and instanced frames byte-identical, with the same hash as before (e22537b0…). Running the old and new loop side by side over seven configurations of seed, frame time, texture size and screen size left every position, velocity and instance transform identical across 297,401 edge reversals.

The two forms of an edge test are not equal for every double, though. y + 16 - 40 < 0 and y < 24 disagree for the one double just below 24: the sum rounds up to 40, so the old loop did not reverse a sprite at exactly that position and the new one does. With the example's 1280x720 window and 32-pixel texture that is the only position where the loops differ. A variant that keeps the sums, and is therefore exact for every double, measured about 21.6 ms per frame; I chose the bounds for the extra 2 ms.

A new unit test pins all four edges: a sprite reverses past an edge but not on it. It passes against the previous loop as well.

At 10,000 sprites the instanced frame is bound by updateBunnies. The loop
read three module-level constants per sprite, each an OP_GET_GLOBAL on
GocciaScript 0.14.0, and added half the texture size in every edge test.

The constants are now locals and the edge tests compare against bounds
computed once per call: 88.76 opcodes per sprite become 78.76, and the
loop falls from 25.6 to 19.1 ms per frame on a Ryzen 9 6900HX.

The rendered frame is unchanged. The bound form differs from the summed
form only for the one double just below the top bound, where the sum
rounded up; benchmarks/import-binding-aliases.md records that and the
measurements. A new unit test pins all four edges.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: c5bb0271-e633-4112-8250-5afe75e1d17a

📥 Commits

Reviewing files that changed from the base of the PR and between 08c3a5b and aad576f.

📒 Files selected for processing (3)
  • benchmarks/import-binding-aliases.md
  • examples/lib/bunny-state.ts
  • tests/unit/bunny-state.test.ts

Limit details: You’ve used the included review currently available. Your 98 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


📝 Walkthrough

Walkthrough

The bunny update loop now uses strict centre-coordinate bounds for bounce checks and caches loop values locally. A unit test checks edge behavior across two updates. Benchmark notes report opcode counts, timings, motion equivalence, and a floating-point edge case.

Changes

Bunny update loop

Layer / File(s) Summary
Update-loop bounds and validation
examples/lib/bunny-state.ts, tests/unit/bunny-state.test.ts, benchmarks/import-binding-aliases.md
updateBunnies reverses velocity when an updated centre coordinate is outside the bounds and caches the stride and position offsets. The test checks velocity and transform coordinates across two updates. The benchmark reports opcode counts, timings, motion equivalence, and a floating-point edge case.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to aad57

The optimization preserves the intended bounce and transform behavior, with a documented floating-point boundary tradeoff. Ready to merge after normal test checks.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main performance improvement to Bunnymark's update loop.
Description check ✅ Passed The description directly explains the update-loop optimizations, benchmark results, equivalence checks, and floating-point behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@frostney
frostney marked this pull request as ready for review October 1, 2026 16:26
@frostney
frostney merged commit bdbdd85 into main Oct 1, 2026
3 checks passed
@frostney
frostney deleted the perf/bunnymark-update-loop branch October 1, 2026 16:32
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.

1 participant