WIP: perf(vk): fix the late pages on the GPU, word by word, instead of waiting for their write-back #248

Draft
GummiNaomi wants to merge 2 commits from GummiNaomi/Longshot:perf/late-pages-fix into main
First-time contributor

Why

In busy places (Lon Lon Ranch by day, crowds, many characters) the game drops from 60 to 50-55 fps, and on some PCs to the 30s, because the renderer waits on the CPU every frame for the GPU to finish writing an earlier frame's results back to game memory: "late pages". At Lon Lon Ranch that wait is ~4.7 ms of a 17.8 ms frame; a player with an RTX 5090 measured ~5.8 ms a frame with 15-30 ms spikes, and an RTX 3080 player saw 31-34 fps (NotRexed/Hookshot#2655, #2789's thread). This removes the wait on PCs whose pools have VRAM mirrors.

What changed

  • The wait becomes a GPU fix-up (gpu/vk/vk_memory.cpp, the block // ---- the late pages' fix (longshot, 2026-11-13) ----; new gpu/vk/shaders/late_fix.comp):
    • take_capture copies the late pages again with no wait. late_fix_build then records, for the write-backs still on the GPU, which words they wrote.
    • late_fix_emit dispatches late_fix.comp, which writes those words into the capture's buffer before anything copies out of it: mirror_sync, mirror_make, the write-back. The result is word for word what the wait's copy gave.
  • Which words (late_fix_build):
    • For the write-back diff pass, per-word stamps: wb_diff.comp now stamps the submission's value on exactly the words it changes, in 64 KB stamp blocks tied to the pool's write-back shadow.
    • For whole copies, a 4 KB snapshot per late page that every whole copy also writes.
    • Pools of 16 MB or less with whole-copied late pages get a shadow instead (+8 MB of VRAM at the ranch).
  • Falls back to the wait, as before, whenever it can't be exact:
    • no stamps or snapshot yet (the first time a page is late);
    • a page written in place by the GPU (note_direct_write, now recorded in wb_direct);
    • both kinds pending;
    • a page whose earlier fix isn't recorded yet;
    • a shadow that changed;
    • no fix table free;
    • a CPU read of the page before its fix (late_fix_cpu_read).
  • The write-back shadow follows the mirror's sync (shadow_sync_copy, shadow_synced_known, RX_VK_SHADOW_FOLLOWS_SYNC). This fixes an existing write-back bug the fix exposed: the diff put stale words back over newer CPU writes every frame. In OoT, the per-character animation records' padding (0x6f6f6f6f) went back over floats the CPU had written, which showed as black flashes in a play test of an earlier version.
  • Off: with RX_VK_LATE_FIX=0, under RX_VK_PARALLEL>1, or when the pipeline can't be made. Capture buffers get storage/address usage only with the fix on.
  • Tests:
    • tests/test_late_fix_build.py: when a capture gets a fix and when it waits.
    • tests/test_wb_shadow_follows_sync.py
    • tests/test_late_fix_shaders.py: the shaders' structs match the C++ ones, and the SPIR-V headers match their sources.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_LATE_FIX =0: the late pages' wait, as before on on: exact by construction (0 differing words in 306 M compared), falls back to the wait where it can't be exact PCs with VRAM mirrors in busy scenes (the wait is per frame there) iGPUs / Steam Deck (no mirror, so no shadow: it waits as before); PCs that never wait (Joey's Linux PC: 0 waits) AMD, Intel, Linux
RX_VK_SHADOW_FOLLOWS_SYNC =0: the write-back shadow is filled from guest memory at the write-back, as before on on: without it the diff writes stale words over the CPU's newer ones any PC with write-back shadows — AMD, Intel, Linux

Both are documented in docs/development.md beside RX_VK_WB_DIFF.

Measurements

WIP: the Longshot A/B (main @ 2e1e2e8c against this PR, the ranch, interleaved) is queued. The numbers below come from the same change on Hookshot, with the switch off and on in one build.

Baseline: Hookshot perf/late-fix @ 56fbc038, RX_VK_LATE_FIX=0 (the wait, as main)
Why not main: measured before the move to Longshot. To be replaced by main @ 2e1e2e8c against this PR.
Hardware: RTX 4070 SUPER 12 GB, NVIDIA 617.42, i7-14700K, 32 GB, Windows 11 Pro 26200
Scene: Lon Lon Ranch at noon, 2560x1440 DLAA, cap 60, unattended scripted run (ranch-day3), 90 s each
Method: the frame trace (RX_VK_FRAME_TRACE=1), 2-second windows at presents 1920-4800; the slow stretch is presents 3840-4320; three runs: on, off, on
How to reproduce: a save on the grass by Lon Lon Ranch, !cheat 112 (noon), stand and turn; RX_VK_FRAME_TRACE=1

Hookshot @ 56fbc038, RX_VK_LATE_FIX=0 (baseline) Hookshot @ 56fbc038, fix on (run 1 / run 2)
fps, slow stretch 53.0 57.4 / 58.3
memory sync a frame, slow stretch 5.04 ms 0.37 / 0.37 ms
late-pages wait a frame (whole run) 1.47 ms 0.08 / 0.10 ms
2-second windows under 55 fps 4 0 / 0
write-back shadows 412 MB 420 MB

On build 134 (before review, same method): fix on 57.9-60.2 fps, median 16.4-16.5 ms, 19-23 frames over 20 ms of 481; off 53.1-55.8 fps, 18.4 ms, 48.

Correctness (a TEMP verify patch, not part of this PR): every late page's bytes after the fix were compared with what the wait would have copied.

vk: TEMP verify: 298800 pages compared: words same 305971200, cpu 0, other 0 (0 on pages written back again since the capture, 0 equal to guest memory now); pages the wait changed 1387 (1400 words, the fix got 1400); pages written back again since the capture 0

Testing

Tested:

  • The game-free checks that read the changed files, on Windows with clang: 39 of 39 pass, including the 3 new tests. Each new test fails when the rule it guards is reverted (20 mutations, one at a time).
  • In the game (Hookshot builds of this change):
    • the TEMP verify above;
    • ranch timing, on and off;
    • the flash detector (RX_VK_FLASH=1) at the ranch and in Kokiri Forest;
    • one player session on an RTX 4070 SUPER: an earlier version showed black flashes, traced to the stale-shadow bug fixed here.
  • Three independent code reviews (GPU ordering, CPU side and locks, tests and rules) found 9 problems, all fixed:
    • a stamp block re-tied to another pool;
    • in-place writes;
    • the stamp block's first recording;
    • mirror_make reading before the fix;
    • two captures of one page;
    • unbounded snapshot ranges;
    • shadow-follows-sync and freed shadows;
    • switch parsing;
    • usage flags with the fix off.

Not tested:

  • Longshot builds of this exact commit in a player's session (queued).
  • AMD, Intel and Linux.
  • Long sessions (snapshot slots are capped at 256 and fall back to the wait once used up).

Needs other testers:

  • AMD (RADV and Windows): the ranch or another busy spot, RX_VK_LATE_FIX=0 against default. Expect the late-pages wait gone (the frame trace's late_wait_ms ~0) and no flashes (request to open).
  • Linux/NVIDIA: as above (request to open).
  • Steam Deck: no change expected (no mirror: falls back to the wait). Confirm nothing breaks (request to open).

Screenshots

Nothing on screen (frame pacing only); flash-detector runs found nothing.

Risk and rollback

A wrong word in a capture would show as a one-frame artifact (a flash). Every case that can't be exact falls back to the old wait, and the verify found 0 differing words. To turn it off: RX_VK_LATE_FIX=0, and RX_VK_SHADOW_FOLLOWS_SYNC=0 for the shadow part. Reverting the commit is also enough.

Reviewer notes

  • Start at late_fix_build (when it fixes and when it waits) and late_fix_emit (where it records).
  • The shadow-follows-sync part fixes an older write-back bug on its own. It can be split into its own fix(vk) PR if you prefer.
  • This answers the review of NotRexed/Hookshot#2789 (the GPU path never ran inside a burst): the fix is recorded lazily in the same part's upload command buffer, so it runs in bursts too. It also merges word by word, not page by page.

Issues

See NotRexed/Hookshot#2655 (the late-pages wait). No Longshot issue yet.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing: the checks reading the changed files ran (39/39). The full run is still to do on this PC, where some unrelated checks don't build (they also fail on main here).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests): requests to open.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Pending: the Longshot A/B.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • A row in docs/tested-configurations.md for each measured run (with the Longshot A/B).
  • The SPIR-V headers regenerated with glslang (a shader changed).
  • A game-free test added for the fix, failing on main and passing here.
## Why In busy places (Lon Lon Ranch by day, crowds, many characters) the game drops from 60 to 50-55 fps, and on some PCs to the 30s, because the renderer waits on the CPU every frame for the GPU to finish writing an earlier frame's results back to game memory: "late pages". At Lon Lon Ranch that wait is ~4.7 ms of a 17.8 ms frame; a player with an RTX 5090 measured ~5.8 ms a frame with 15-30 ms spikes, and an RTX 3080 player saw 31-34 fps (NotRexed/Hookshot#2655, #2789's thread). This removes the wait on PCs whose pools have VRAM mirrors. ## What changed - **The wait becomes a GPU fix-up** (`gpu/vk/vk_memory.cpp`, the block `// ---- the late pages' fix (longshot, 2026-11-13) ----`; new `gpu/vk/shaders/late_fix.comp`): - `take_capture` copies the late pages again with no wait. `late_fix_build` then records, for the write-backs still on the GPU, which words they wrote. - `late_fix_emit` dispatches `late_fix.comp`, which writes those words into the capture's buffer before anything copies out of it: `mirror_sync`, `mirror_make`, the write-back. The result is word for word what the wait's copy gave. - **Which words** (`late_fix_build`): - For the write-back diff pass, per-word stamps: `wb_diff.comp` now stamps the submission's value on exactly the words it changes, in 64 KB stamp blocks tied to the pool's write-back shadow. - For whole copies, a 4 KB snapshot per late page that every whole copy also writes. - Pools of 16 MB or less with whole-copied late pages get a shadow instead (+8 MB of VRAM at the ranch). - **Falls back to the wait, as before,** whenever it can't be exact: - no stamps or snapshot yet (the first time a page is late); - a page written in place by the GPU (`note_direct_write`, now recorded in `wb_direct`); - both kinds pending; - a page whose earlier fix isn't recorded yet; - a shadow that changed; - no fix table free; - a CPU read of the page before its fix (`late_fix_cpu_read`). - **The write-back shadow follows the mirror's sync** (`shadow_sync_copy`, `shadow_synced_known`, `RX_VK_SHADOW_FOLLOWS_SYNC`). This fixes an existing write-back bug the fix exposed: the diff put stale words back over newer CPU writes every frame. In OoT, the per-character animation records' padding (`0x6f6f6f6f`) went back over floats the CPU had written, which showed as black flashes in a play test of an earlier version. - **Off:** with `RX_VK_LATE_FIX=0`, under `RX_VK_PARALLEL>1`, or when the pipeline can't be made. Capture buffers get storage/address usage only with the fix on. - **Tests:** - `tests/test_late_fix_build.py`: when a capture gets a fix and when it waits. - `tests/test_wb_shadow_follows_sync.py` - `tests/test_late_fix_shaders.py`: the shaders' structs match the C++ ones, and the SPIR-V headers match their sources. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_LATE_FIX` | `=0`: the late pages' wait, as before | on | on: exact by construction (0 differing words in 306 M compared), falls back to the wait where it can't be exact | PCs with VRAM mirrors in busy scenes (the wait is per frame there) | iGPUs / Steam Deck (no mirror, so no shadow: it waits as before); PCs that never wait (Joey's Linux PC: 0 waits) | AMD, Intel, Linux | | `RX_VK_SHADOW_FOLLOWS_SYNC` | `=0`: the write-back shadow is filled from guest memory at the write-back, as before | on | on: without it the diff writes stale words over the CPU's newer ones | any PC with write-back shadows | — | AMD, Intel, Linux | Both are documented in `docs/development.md` beside `RX_VK_WB_DIFF`. ## Measurements **WIP: the Longshot A/B (main @ 2e1e2e8c against this PR, the ranch, interleaved) is queued.** The numbers below come from the same change on Hookshot, with the switch off and on in one build. Baseline: `Hookshot perf/late-fix @ 56fbc038, RX_VK_LATE_FIX=0` (the wait, as main) Why not main: measured before the move to Longshot. To be replaced by main @ 2e1e2e8c against this PR. Hardware: RTX 4070 SUPER 12 GB, NVIDIA 617.42, i7-14700K, 32 GB, Windows 11 Pro 26200 Scene: Lon Lon Ranch at noon, 2560x1440 DLAA, cap 60, unattended scripted run (`ranch-day3`), 90 s each Method: the frame trace (`RX_VK_FRAME_TRACE=1`), 2-second windows at presents 1920-4800; the slow stretch is presents 3840-4320; three runs: on, off, on How to reproduce: a save on the grass by Lon Lon Ranch, `!cheat 112` (noon), stand and turn; `RX_VK_FRAME_TRACE=1` | | Hookshot @ 56fbc038, RX_VK_LATE_FIX=0 (baseline) | Hookshot @ 56fbc038, fix on (run 1 / run 2) | |---|---|---| | fps, slow stretch | 53.0 | 57.4 / 58.3 | | memory sync a frame, slow stretch | 5.04 ms | 0.37 / 0.37 ms | | late-pages wait a frame (whole run) | 1.47 ms | 0.08 / 0.10 ms | | 2-second windows under 55 fps | 4 | 0 / 0 | | write-back shadows | 412 MB | 420 MB | On build 134 (before review, same method): fix on 57.9-60.2 fps, median 16.4-16.5 ms, 19-23 frames over 20 ms of 481; off 53.1-55.8 fps, 18.4 ms, 48. **Correctness** (a TEMP verify patch, not part of this PR): every late page's bytes after the fix were compared with what the wait would have copied. ``` vk: TEMP verify: 298800 pages compared: words same 305971200, cpu 0, other 0 (0 on pages written back again since the capture, 0 equal to guest memory now); pages the wait changed 1387 (1400 words, the fix got 1400); pages written back again since the capture 0 ``` ## Testing **Tested:** - The game-free checks that read the changed files, on Windows with clang: 39 of 39 pass, including the 3 new tests. Each new test fails when the rule it guards is reverted (20 mutations, one at a time). - In the game (Hookshot builds of this change): - the TEMP verify above; - ranch timing, on and off; - the flash detector (`RX_VK_FLASH=1`) at the ranch and in Kokiri Forest; - one player session on an RTX 4070 SUPER: an earlier version showed black flashes, traced to the stale-shadow bug fixed here. - Three independent code reviews (GPU ordering, CPU side and locks, tests and rules) found 9 problems, all fixed: - a stamp block re-tied to another pool; - in-place writes; - the stamp block's first recording; - `mirror_make` reading before the fix; - two captures of one page; - unbounded snapshot ranges; - shadow-follows-sync and freed shadows; - switch parsing; - usage flags with the fix off. **Not tested:** - Longshot builds of this exact commit in a player's session (queued). - AMD, Intel and Linux. - Long sessions (snapshot slots are capped at 256 and fall back to the wait once used up). **Needs other testers:** - AMD (RADV and Windows): the ranch or another busy spot, `RX_VK_LATE_FIX=0` against default. Expect the late-pages wait gone (the frame trace's `late_wait_ms` ~0) and no flashes (request to open). - Linux/NVIDIA: as above (request to open). - Steam Deck: no change expected (no mirror: falls back to the wait). Confirm nothing breaks (request to open). ## Screenshots Nothing on screen (frame pacing only); flash-detector runs found nothing. ## Risk and rollback A wrong word in a capture would show as a one-frame artifact (a flash). Every case that can't be exact falls back to the old wait, and the verify found 0 differing words. To turn it off: `RX_VK_LATE_FIX=0`, and `RX_VK_SHADOW_FOLLOWS_SYNC=0` for the shadow part. Reverting the commit is also enough. ## Reviewer notes - Start at `late_fix_build` (when it fixes and when it waits) and `late_fix_emit` (where it records). - The shadow-follows-sync part fixes an older write-back bug on its own. It can be split into its own `fix(vk)` PR if you prefer. - This answers the review of NotRexed/Hookshot#2789 (the GPU path never ran inside a burst): the fix is recorded lazily in the same part's upload command buffer, so it runs in bursts too. It also merges word by word, not page by page. ## Issues See NotRexed/Hookshot#2655 (the late-pages wait). No Longshot issue yet. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [ ] `python -I -B tests/run_all.py` run and passing: the checks reading the changed files ran (39/39). The full run is still to do on this PC, where some unrelated checks don't build (they also fail on main here). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests): requests to open. - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. **Pending: the Longshot A/B.** - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [ ] A row in `docs/tested-configurations.md` for each measured run (with the Longshot A/B). - [x] The SPIR-V headers regenerated with glslang (a shader changed). - [x] A game-free test added for the fix, failing on `main` and passing here.
Ported from Hookshot perf/late-fix-main (b8e4ffda) onto Longshot main as one change:
the late pages' fix (shaders/late_fix.comp, per-word stamps from wb_diff.comp, 4 KB page
snapshots for whole copies, small pools given a shadow), the write-back shadow following
the mirror's sync (RX_VK_SHADOW_FOLLOWS_SYNC), the fixes from review, and the game-free
tests (test_late_fix_build, test_wb_shadow_follows_sync, test_late_fix_shaders).
RX_VK_LATE_FIX=0: the wait, as before.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs: RX_VK_LATE_FIX and RX_VK_SHADOW_FOLLOWS_SYNC in the switch reference
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
1c567b5a73
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HumanBean added this to the v3.0.0 milestone 2026-11-13 17:46:05 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'perf/late-pages-fix:GummiNaomi-perf/late-pages-fix'
git switch 'GummiNaomi-perf/late-pages-fix'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!248
No description provided.