Skip to content

perf(lift): one page mark per straight-line group of stores through a base register - #912

Merged
hckrman101 merged 5 commits into
NotRexed:mainfrom
klimovich008:perf-store-mark-groups
Nov 9, 2026
Merged

hckrman101 merged 5 commits into
NotRexed:mainfrom
klimovich008:perf-store-mark-groups

Conversation

@klimovich008

@klimovich008 klimovich008 commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What this fixes

With A64_WRITE_TRACK, every non-SP guest store marks its page with two release byte stores. Lifted code spends a measurable share of game-thread time on those marks.

Why merge

A group of stores through one base register, in a straight-line run spanning at most 4 KB, now gets one shared A64_MARK. It covers the same two pages, placed before anything that ends the run. Mark stores drop from 2,521,896 to 1,454,508 (−42.3%) on lifted v2.0.6 code.

Change

  • nx2/lift.py edit_c runs the text patches first, then a64_lift.group_marks once over the written C. This keeps oot_patches.py's exact-text edits intact (the review's blocker).
  • On the supported build's full lift, each output-size edit must apply, or be already applied, exactly once. Otherwise the lift stops before compiling. Other patches still only warn.
  • What ends a run: labels, branches, returns, unbalanced braces, STLR/LDAR, and any call not on a short list of plain loads, stores and pure helpers.
  • chunks.write clears an earlier lift's hot pieces before patching.
  • A64_MARK_EACH=1 turns grouping off.

Tested

tests/test_mark_groups.py, 21 cases:

  • patches-then-groups through edit_c, with the old order as a control that must fail;
  • the patched function compiled and run with and without groups, with the same memory and pages;
  • address forms, the 4 KB span, a hot rebuild, and duplicate or mixed patch counts.

All 21 deliberate sabotages fail it.

Not covered

  • test_insn/test_leaf not run (needs Unicorn and a game dump). No full lift or game run of this revision.
  • The earlier CPU result (68.0 vs 69.7 ms/frame) was a batch build. It doesn't isolate this change.

🤖 Generated with Claude Code

klimovich008 and others added 3 commits November 9, 2026 19:42
… base register

A64_WRITE_TRACK marks the page of every non-SP store, two byte stores and
the address arithmetic each. In a straight-line run (no label, branch, call,
return, poll, barrier, atomic, exclusive, SVC or trap) the stores through
one base register plus constants spanning at most 4 KB now share one
A64_MARK of their byte span, placed after the group's last data store and
before the base register changes or the run ends. The pages marked are the
same, and each data store is still followed by a release store of its
page's mark before any guest synchronisation.

43% fewer mark stores in the lifted game, about 24 MB less code.
A64_MARK_EACH=1 at lift time keeps a mark per store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…edit stops the lift

Review of NotRexed#912. The groups changed the lifted text before nx2/oot_patches.py
matched it exactly: OUT_EDITS[0] (nnMain's create arg) no longer matched,
apply_output_size only warned, and the Output resolution was ignored.

- Order: emit_function no longer groups. nx2/lift.py edit_c applies the
  patches, then a64_lift.group_marks over the written code_*.c files, last,
  once, with the lift's stats (reported). The kit's own write() keeps a mark
  per store; A64_MARK_EACH=1 still turns the groups off (in the lift key).
- Required edits: on the supported build's whole standalone lift, every
  OUT_EDITS place must be patched, or found patched, exactly once, or the
  lift stops before the key is written. Other builds, partial lifts and the
  other patches warn as before.
- Run ends, conservative: a line whose braces do not balance (no mark moves
  into or out of a multi-line block), STLR/LDAR in suyu's spelling, and any
  call-like token not on a short list of plain loads, stores and pure
  helpers (f_*, hos_* from the patches, A64_*, atomics, unknown helpers).
- The pass is idempotent (its own marks are not run ends), takes the nearest
  _b/_a definition on a line, skips functions with local registers, and
  shares REG_WRITE (now with ++/--) with reg_writes. About 6x faster than
  before: functions with fewer than two stores skipped, lines with no open
  group skipped, cheap substring tests before each regex.
- Lifted v2.0.6 C: mark stores 2,521,896 -> 1,454,508 (42.3% fewer; the
  first version 43.1%). Idempotent on all 80 files.
- tests/test_mark_groups.py: the patches-then-groups sequence through
  lift.edit_c with the OUT_EDITS places (no game code), the old order as a
  failing control, the patched function compiled and run with and without
  groups (same memory, same pages), the review's address forms, the 4096
  span, STLR/LDAR, blocks, host calls, A64_MARK_EACH, the lift key, stats,
  idempotence, and a random page-set check. nx2/test_leaf.py groups its
  functions as the lift does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt every made and raw output-size place

Review of NotRexed#912 (review229).

- chunks.write removes an earlier lift's code_hot_*.c and code_hot.c.whole
  whether or not this lift writes a code_hot.c. With a hot list, the old
  pieces (patched already) stayed until make_game's split_hot, after the
  lift: nx2/oot_patches.py and the groups read them beside the new
  code_hot.c, an output-size place counted twice, and the required check
  failed a normal rebuild. split_hot makes this lift's pieces after it.
- apply_output_size counts each made place and each raw place outside a
  made one (a new text can hold its old one), per file. A file with a
  place made twice, or made and raw, was counted once and accepted; now
  it is not once, and the supported build's lift stops. A file made once
  is accepted and left as it is.
- tests/test_mark_groups.py: a rebuild over the first lift's patched hot
  pieces (chunks.write, edit_c, split_hot, twice) finds each place once;
  made once accepted; made twice, made and raw ([3], and [4] whose new
  text holds its old one), and raw in another file rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@klimovich008 klimovich008 changed the title Lift: one page mark per straight-line group of stores through a base register perf(lift): one page mark per straight-line group of stores through a base register Nov 9, 2026
@klimovich008
klimovich008 force-pushed the perf-store-mark-groups branch from b7d5a93 to 816a472 Compare November 9, 2026 18:43
@klimovich008

klimovich008 commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks, all addressed in 3fa6300 and 816a472 (rebased on current main):

  1. Blocker: the lift no longer groups. nx2/lift.py edit_c runs oot_patches, then a64_lift.group_marks once over the written code_*.c. On real v2.0.6 C taken back to its unpatched text, the old order loses OUT_EDITS[0] and the new order patches it.
  2. Required edits: on the supported build's whole standalone lift, each OUT_EDITS place must be found exactly once, or the lift stops before its key is written. Other builds, --max-func and the other patches only warn.
  3. Counting: made places plus raw places outside made ones, per file. Made twice, or made and raw, is rejected; made once is accepted.
  4. Hot rebuilds: chunks.write removes the earlier lift's code_hot_*.c before any patcher runs, so the old patched pieces aren't counted again.
  5. STLR/LDAR: recomp_storeN(c,c->x[N], and recomp_loadN(c,c->x[N]) end the run.
  6. Braces: a line whose braces don't balance ends the run. One-line { _b ... } blocks still group.
  7. Calls: any call-like token outside a short list of plain loads, stores and pure helpers ends the run (f_, hos_, A64_*, atomics, unknown helpers).
  8. Stats: L.stats is passed in and printed by report(). There's one REG_WRITE (now with ++/--).
  9. Perf: about 6x faster (0.93 s vs 5.7 s per 24 MB chunk), roughly 55 s per full lift.
  10. Tests: your list, MARK_EACH, idempotence, a page-set check, a hot rebuild, the count cases, and the patched function compiled and run with and without groups. The old order is kept as a failing control, and 21 sabotages each fail.
  11. Benefit: 2,521,896 to 1,454,508 mark stores on v2.0.6 C (-42.3%, vs -43.1% before).

Not rerun here: test_insn and test_leaf (no capstone/unicorn or game), and no game run.

klimovich008 and others added 2 commits November 9, 2026 20:18
nx2/lift.py imports nso.py, which imports lz4 for NSO decompression; the
Python source checks have no lz4, so the three tests through lift.edit_c
failed at import. They read no NSO: lz4 is stood in for like capstone.
(test_output_resolution's hos_rt_pool_size assertion fails on main too.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s tools

The order tests imported nx2/lift.py for edit_c, and lift.py imports the
whole lifter at load time (capstone, nso.py's lz4, adr_entries' numpy):
the CI runners have neither lz4 nor numpy, so the three tests of the
patches-then-groups order errored out. edit_c (the patches, then the
groups, over the written code_*.c) now lives beside chunks.write, which
already writes those files and imports a64_lift; lift.py calls
chunks.edit_c. The lz4/capstone stand-ins in the test go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@klimovich008

Copy link
Copy Markdown
Contributor Author

Thanks. Fixed in 1c5258f: stubbing lz4 got past that import, but lift.py also pulls in numpy (via adr_entries) and the rest of the lifter at load time. So edit_c now lives in nx2/chunks.py, beside chunks.write, which already writes those files and imports a64_lift. lift.py calls chunks.edit_c, and the tests import chunks directly with no module stand-ins. CI is green on both runners now, and the three order tests run there.

On the nits: yes, the supported lift stopping under A64_LOCAL_REGS is intended, since its output-size edits can't apply then. And the costs from before grouping in chunk_costs.txt are fine as a slight overestimate.

@hckrman101
hckrman101 merged commit 9c74f8f into NotRexed:main Nov 9, 2026
10 checks passed
github-actions Bot pushed a commit to notquiteog/Hookshot that referenced this pull request Nov 9, 2026
## [2.7.0](v2.6.4...v2.7.0) (2026-11-09)

### Features

* microphone input for the ocarina's input streams ([8a17b8c](8a17b8c))
* microphone input on SDL2 too (the Linux AppImage's) ([eadc184](eadc184))
* **vk:** AMD FSR 4 upscaling and frame generation through AMD's runtime ([65003cc](65003cc))
* **vk:** say why DLSS 5 neural rendering is refused, and the RTX 20-40 route ([60f1d2e](60f1d2e))

### Bug Fixes

* **audio:** 5.1 surround when the default output has it ([NotRexed#1124](https://github.com/notquiteog/Hookshot/issues/1124)) ([b0abeca](b0abeca))
* **audio:** a finished wave buffer reaches the game at the renderer update, not mid-pass ([28e4a11](28e4a11))
* **builder:** stop when generated source cleanup fails ([d254e6f](d254e6f))
* contain pipeline cache save allocation failures ([4518a20](4518a20)), closes [NotRexed#1022](https://github.com/notquiteog/Hookshot/issues/1022)
* **lift:** clear an earlier lift's hot pieces before the patches; count every made and raw output-size place ([816a472](816a472)), closes [NotRexed#912](https://github.com/notquiteog/Hookshot/issues/912)
* **lift:** edit_c in chunks.py, so its tests import without the lift's tools ([1c5258f](1c5258f))
* **lift:** group page marks after the patches; a missing output-size edit stops the lift ([3fa6300](3fa6300))
* microphone names past 60 bytes, unplugs that keep sending ([NotRexed#937](https://github.com/notquiteog/Hookshot/issues/937), third review) ([e77bd79](e77bd79))
* of two microphones with one name, the one chosen opens ([602d96e](602d96e))
* preserve NUP member names during implicit reads ([1fdfeff](1fdfeff))
* **release:** the Windows release installs the renderer CI built, with DLSS, NR and FSR 4 ([0fa18bd](0fa18bd))
* the microphone retry path ([NotRexed#937](https://github.com/notquiteog/Hookshot/issues/937), second review) ([50f2392](50f2392))
* the microphone review's points ([NotRexed#937](https://github.com/notquiteog/Hookshot/issues/937)) ([27a9b7d](27a9b7d))
* **vk:** a draw that waits for its pipeline runs the build at once if it is still queued ([aafbaf5](aafbaf5))
* **vk:** keep the last sRGB UNORM surface format, so drivers that list only sRGB keep theirs ([3408437](3408437))
* **vk:** Linux/Wayland: take an sRGB surface format for the swapchain ([7416487](7416487))
* **vk:** no mirror is made inside a parallel replay's segment (an earlier segment read it unfilled) ([8f0ed68](8f0ed68))
* **vulkan:** preserve guest mip levels in partial clears ([d7a1207](d7a1207))
* **vulkan:** release modules from rejected cache records ([2838e89](2838e89))
* **widescreen:** the save picture shows the middle 16:9 on ultrawide ([NotRexed#952](https://github.com/notquiteog/Hookshot/issues/952)) ([e2e155e](e2e155e))

### Performance Improvements

* **fpx:** keep the hardware result for FMUL/FMLA S when the flags are already set ([6de3bf1](6de3bf1))
* **lift:** one page mark per straight-line group of stores through a base register ([825861b](825861b))
* stream sampled profiles instead of loading every line ([8cf6d1e](8cf6d1e))
github-actions Bot pushed a commit to notquiteog/Hookshot that referenced this pull request Nov 9, 2026
## [2.0.12](v2.0.11...v2.0.12) (2026-11-09)

### Features

* a timed span per FSR pass, so the cost has a number per pass ([1d7f262](1d7f262))
* add a Quit option to the gameplay menu ([0b02149](0b02149))
* **audio:** Audio output in the Insert menu: Auto, Stereo or 5.1 ([4856258](4856258)), closes [NotRexed#1182](https://github.com/notquiteog/Hookshot/issues/1182)
* census the render target sets, to settle whether a G-buffer exists ([8f2fda3](8f2fda3))
* compile AMD's FSR 3.1 upscaler passes from the FidelityFX SDK ([37afa2e](37afa2e)), closes [NotRexed#577](https://github.com/notquiteog/Hookshot/issues/577)
* compose the scene's view projection, and transpose it for the shader ([df82d90](df82d90))
* count FSR evaluations, because nine passes a call is cheap or ruinous ([85a3ea2](85a3ea2))
* create FSR 3.1's pipelines and internal images ([6b51433](6b51433))
* derive camera motion from the scene's depth, and keep that depth ([29c7cbe](29c7cbe))
* DLSS at the seam, and a stage switch for the FSR cost ([6aa9f48](6aa9f48))
* DLSS Super Resolution through NGX, behind a build-time SDK gate ([aff6abb](aff6abb))
* frame generation asks for its own extensions and the whole input set ([ad291d1](ad291d1))
* FSR 3 frame interpolation, and a jitter for the upscalers to undo ([384c41b](384c41b))
* give FSR the assembled depth and camera motion ([481b957](481b957))
* HOS_DISPLAY_VRR, which is what actually holds the frame rate at 60 ([c65c68e](c65c68e))
* HOS_FPS_FORCE, because raising the limiter alone does not hold ([2008ac7](2008ac7))
* HOS_VSYNC_HZ, the hardcoded 60 that actually paces the game ([5eb9f8b](5eb9f8b))
* link NGX properly, frame generation at the seam, AMD's interpolation sources ([43e3360](43e3360))
* Monitor's shape for screens narrower than 16:9 (4:3, 16:10, 5:4) ([b44c9cd](b44c9cd))
* name each network binding's role in the upscaler input contract ([d9566a1](d9566a1))
* publish Ocarina's camera, and make a backend reachable without the stand-in ([8bd7c8d](8bd7c8d))
* record FSR 3.1's nine dispatches ([8769c80](8769c80))
* route the game's upscale dispatch through one backend seam ([1a036c8](1a036c8))
* RX_VK_VP_HUNT, to find the view projection from a value only it holds ([3c02213](3c02213))
* the hunt found the projection, so widen it to find the view ([2946a32](2946a32))
* the parallel lock says which site took it, and the answer is two sites ([b8543c9](b8543c9))
* the two backends have rows in the Insert menu ([cf8b3b1](cf8b3b1))
* the two lock sites say why they had to take it ([614f552](614f552))
* the upscaler and frame generation follow the menu while you play ([61d7d4a](61d7d4a))
* the upscalers and frame generation are the native settings, not new rows ([a0ce7de](a0ce7de))
* the vsync event follows the display, and 0 turns it off ([a67e471](a67e471))
* **vk:** AMD FSR 4 upscaling and frame generation through AMD's runtime ([65003cc](65003cc))
* **vk:** choose the frame generation method in the Insert menu ([c4f7d84](c4f7d84))
* **vk:** DLSS 5 neural rendering through NGX directly, beside Streamline ([7784399](7784399))
* **vk:** DLSS 5 neural rendering, with the model's own settings in the menu ([2851bc6](2851bc6))
* **vk:** frame generation Auto: a frame for each of the display's refreshes ([a5a7440](a5a7440))
* **vk:** say why DLSS 5 neural rendering is refused, and the RTX 20-40 route ([60f1d2e](60f1d2e))
* **vk:** time a capture's GPU wait for its late pages apart in the log ([e3b622f](e3b622f))

### Bug Fixes

* a fork releases its own work, not upstream's ([47dd097](47dd097))
* a Windows build could ask for DLSS and then fail to link ([d72441a](d72441a))
* **audio:** 5.1 surround when the default output has it ([NotRexed#1124](https://github.com/notquiteog/Hookshot/issues/1124)) ([b0abeca](b0abeca))
* **audio:** a finished wave buffer reaches the game at the renderer update, not mid-pass ([28e4a11](28e4a11))
* **build:** a Windows build linked the ARM64 NGX library, not the x64 one ([70b7d25](70b7d25))
* **builder:** copies from a read-only install stay writable ([70ffa5e](70ffa5e))
* **builder:** stop when generated source cleanup fails ([d254e6f](d254e6f))
* **builder:** the window icon is copied writable too ([4b2d871](4b2d871))
* **build:** NGX asks for Microsoft's C++ runtime, which llvm-mingw has not ([114e917](114e917))
* **build:** the feature announcement swallowed the whole Vulkan build ([1004f37](1004f37))
* **build:** the twelve MSVC runtime symbols NGX asks of an llvm-mingw link ([d00159a](d00159a))
* clear the SPD counter between the pyramid passes (did not fix the cost) ([66e8c3a](66e8c3a))
* contain pipeline cache save allocation failures ([4518a20](4518a20)), closes [NotRexed#1022](https://github.com/notquiteog/Hookshot/issues/1022)
* HOS_FPS_FORCE must not touch the nominal rate: it is slow motion on screen ([8681f2e](8681f2e))
* **lift:** clear an earlier lift's hot pieces before the patches; count every made and raw output-size place ([816a472](816a472)), closes [NotRexed#912](https://github.com/notquiteog/Hookshot/issues/912)
* **lift:** edit_c in chunks.py, so its tests import without the lift's tools ([1c5258f](1c5258f))
* **lift:** group page marks after the patches; a missing output-size edit stops the lift ([3fa6300](3fa6300))
* **linux:** the AppImage's parts list names the repository it is built in ([2228816](2228816))
* narrow screens: window at the picture's shape, save picture's middle band, comments ([3060261](3060261)), closes [NotRexed#1183](https://github.com/notquiteog/Hookshot/issues/1183)
* preserve NUP member names during implicit reads ([1fdfeff](1fdfeff))
* publish the camera from the inlined projection copies too ([c630d59](c630d59))
* read the upscaler contract in descriptor order, not from draw_sampled ([492bcd0](492bcd0))
* **release:** the fork's releases follow upstream's v2.0.x line ([d495994](d495994))
* **release:** the setup, the Builder and the game update from the repository they were built in ([6fd4364](6fd4364))
* **release:** the Windows release installs the renderer CI built, with DLSS, NR and FSR 4 ([0fa18bd](0fa18bd))
* scan for the view projection once a target set, not once a draw ([12d4d83](12d4d83))
* snapshot the projection's fields, and name what caps the frame rate ([3790556](3790556))
* the conventionalcommits preset has to match the changelog writer ([ca6595f](ca6595f))
* the DLSS contract test links again ([86b6988](86b6988))
* the game's network takes one texture, so it is not a temporal upscaler ([853cfe3](853cfe3))
* the history reset the document specifies, which nothing implemented ([9858599](9858599))
* the host camera symbols must be weak, or the renderer does not load at all ([70ecdf4](70ecdf4))
* the release's commit-analyzer preset is a package, and it was not declared ([3ff9fbd](3ff9fbd))
* the renderer would not load, and the build now says so ([c871a3c](c871a3c))
* the shader check compared glslang's version, and ran where glslang is not ([f48483c](f48483c))
* the view projection scan ran per draw, which stuttered hard ([2d9b4e8](2d9b4e8))
* **vk:** a draw that waits for its pipeline runs the build at once if it is still queued ([aafbaf5](aafbaf5))
* **vk:** DLSS frame generation sets NGX's parameters through its C functions ([2c19b63](2c19b63))
* **vk:** frame generation Auto: the snap distance is not named near (windows.h defines it empty) ([d156a6d](d156a6d))
* **vk:** keep the last sRGB UNORM surface format, so drivers that list only sRGB keep theirs ([3408437](3408437))
* **vk:** Linux/Wayland: take an sRGB surface format for the swapchain ([7416487](7416487))
* **vk:** no mirror is made inside a parallel replay's segment (an earlier segment read it unfilled) ([8f0ed68](8f0ed68))
* **vk:** tag the uplift colour pair as whole resources, as the shipping integration does ([dd3e61e](dd3e61e))
* **vulkan:** bind only bytes accessed by buffer-image copies ([b0a9034](b0a9034))
* **vulkan:** clean up failed mesh argument initialization ([b2f5692](b2f5692))
* **vulkan:** preserve guest mip levels in partial clears ([d7a1207](d7a1207))
* **vulkan:** release modules from rejected cache records ([2838e89](2838e89))
* **widescreen:** the save picture shows the middle 16:9 on ultrawide ([NotRexed#952](https://github.com/notquiteog/Hookshot/issues/952)) ([e2e155e](e2e155e))

### Performance Improvements

* **fpx:** keep the hardware result for FMUL/FMLA S when the flags are already set ([6de3bf1](6de3bf1))
* **lift:** one page mark per straight-line group of stores through a base register ([825861b](825861b))
* **lift:** the interrupt poll before each backward goto, calling a64_poll_cur() with no argument, so a loop's guest registers stay in host registers ([0d6ed03](0d6ed03))
* stream sampled profiles instead of loading every line ([8cf6d1e](8cf6d1e))
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