WIP: perf(vk): sparse VRAM mirrors, memory only where a game pool is (RX_VK_MIRROR_SPARSE=1) #250

Draft
GummiNaomi wants to merge 1 commit from GummiNaomi/Longshot:perf/sparse-mirror into main
First-time contributor

Why

On cards short of video memory, the buffer pools' VRAM mirrors hold hundreds of MB that no game pool is in: 243-533 MB of 1.1-1.6 GB at 2560x1440 DLAA. That pushes textures out and costs frame rate on 6-8 GB cards. This makes the mirrors sparse, opt-in, so memory is held only in the 64 KB blocks a game pool is in. It was NotRexed/Hookshot#2794 before the move; it may also help #130 (a GPU reset at the VRAM limit on an 8 GB RX 6600).

What changed

  • gpu/vk/sparse_mirror.h (new): which blocks a game pool is in, block spans, runs, and copies clipped to blocks with memory.
  • gpu/vk/vk_vma.cpp: a sparse buffer, and its blocks' memory from VMA (counted in its budget). vmaFreeMemory joins the recorder's drain boundaries (gpu/vk/vk_recorder.h).
  • gpu/vk/vk_memory.cpp:
    • mirror_make: a sparse buffer with memory for the blocks a game pool is in. The page table points blocks with none at guest memory, as for a pool with no mirror.
    • gpu_buffer_bind: a binding that reaches a block with no memory is served from guest memory for that submission, and the block is wanted.
    • sparse_grow: at each submission's start, the wanted blocks and the blocks newly in a pool get memory in one batched bind, filled as a new mirror is.
    • sparse_release: a block no pool or binding has reached for 600 submissions gives its memory back, once the GPU is past it.
    • Binds signal a timeline semaphore that the recordings wait for on the GPU, not on the CPU (RX_VK_SPARSE_WAIT=1: on the CPU, as before).
    • Write-backs and a retired mirror's inheritance are clipped to blocks with memory.
  • gpu/vk/vk_replay.cpp: submissions of recordings wait for the binds' semaphore.
  • Tests: tests/test_sparse_mirror.py (+ tests/test_sparse_mirror.cpp). test_direct_buffer_writes and test_readback_part were updated for the new code they slice.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_MIRROR_SPARSE =1: sparse VRAM mirrors, memory only in the blocks a game pool is in (needs strict sparse residency for buffers; off otherwise, logged) off off by default; on after AMD/Intel testing cards near their VRAM limit (6-8 GB) cards with plenty of VRAM (no fps change at 12 GB) AMD (RADV, Windows), Intel, Linux
RX_VK_SPARSE_WAIT =1: each sparse bind waited for on the CPU, as before off off — — —

Documented in docs/development.md's switch reference.

Measurements

WIP: the Longshot A/B (main @ 2e1e2e8c against this PR with RX_VK_MIRROR_SPARSE=1) is queued. The numbers below are from Hookshot: the same code, one build, the switch off and on.

Baseline: Hookshot perf/sparse-mirror @ 130ee5e6, RX_VK_MIRROR_SPARSE unset
Why not main: measured on Hookshot before the move.
Hardware: RTX 4070 SUPER 12 GB, NVIDIA 617.42, i7-14700K, 32 GB, Windows 11 Pro 26200 (6 and 8 GB cards simulated with RX_VK_TEST_ALLOWED_MB)
Scene: Hyrule Field walk, Castle Town to the field, Kokiri Forest; 2560x1440 DLAA, unattended
Method: frame trace and VRAM log lines, three pairs per scene where stated

baseline (unset) RX_VK_MIRROR_SPARSE=1
6 GB allowance (RX_VK_TEST_ALLOWED_MB=6144, RX_VK_MIRROR_MB=1536), field walk, three pairs: fps 47.7, 51.5, 49.9 57.3, 55.3, 58.2
the same: frames over 25 ms 3,042, 1,840, 2,114 625, 1,160, 408
the same: VRAM peak 7,327, 6,254, 6,444 MB 6,394, 6,200, 6,387 MB
8 GB allowance (=6900), field walk: fps / VRAM peak 59.0-59.1 / 7,028-7,138 MB 59.1-59.3 / 6,676-7,018 MB
Castle Town to the field: the load's longest frame 816-902 ms 866-961 ms
Kokiri Forest: VRAM mean 8,215 MB 7,462-7,507 MB

A real 8 GB card (klikso, on NotRexed/Hookshot#2794): RTX 3060 Ti 8 GB, 2560x1440 DLSS K. About 430 MB less in the mirrors, 120-370 MB less VRAM in Castle Town Market, and a higher texture budget. Frame rate and long frames were the same within three pairs; nothing broke.

Testing

Tested:

  • The game-free checks that read the changed files: 109 pass. 8 fail, and those same 8 fail on Longshot main on this PC (checked with the same files touched): test_audio_volume, test_oot_fov, test_opening_skip, test_present_aspect_vulkan, test_present_thread, test_readback_wait_bounded, test_text_size, test_texture_staging_publication. They don't build with this PC's compilers.
  • In the game:
    • the runs above;
    • screenshots of Castle Town, Kokiri and the field look the same on and off;
    • no device lost and no write-back warning in any run;
    • the player's own sessions with it on, in Hookshot builds since 12 Nov.
  • Two independent code reviews of earlier versions found ten problems, all fixed.

Not tested:

  • AMD (RADV and Windows), Intel, Linux.
  • Long sessions on a real 6 GB card.
  • Longshot builds of this exact commit (queued).

Needs other testers:

  • AMD RADV (Linux), a 6-8 GB card: the field walk on and off. Expect lower VRAM, equal or higher fps, and no lost device (request to open).
  • AMD on Windows: the same (request to open).
  • Intel Arc: the same, or the log line saying it's off when the device lacks strict sparse residency (request to open).

Screenshots

Same spot, off and on: identical. To add from the Longshot A/B.

Risk and rollback

Off by default. When on, a block that is wanted but not bound yet is served from guest memory for one submission, as a pool with no mirror always is. A driver bug in sparse binding could lose the device. Unset RX_VK_MIRROR_SPARSE to turn it off.

Reviewer notes

Start with gpu/vk/sparse_mirror.h, then sparse_grow and sparse_release in vk_memory.cpp.

Issues

See #130

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. 8 fail here and fail on main here too (named above). The full run is still to do.
  • 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 (the Hookshot runs; Longshot rows to add with the A/B).
  • A check under tests/ that reads changed source updated in this PR, not weakened.
## Why On cards short of video memory, the buffer pools' VRAM mirrors hold hundreds of MB that no game pool is in: 243-533 MB of 1.1-1.6 GB at 2560x1440 DLAA. That pushes textures out and costs frame rate on 6-8 GB cards. This makes the mirrors sparse, opt-in, so memory is held only in the 64 KB blocks a game pool is in. It was NotRexed/Hookshot#2794 before the move; it may also help #130 (a GPU reset at the VRAM limit on an 8 GB RX 6600). ## What changed - `gpu/vk/sparse_mirror.h` (new): which blocks a game pool is in, block spans, runs, and copies clipped to blocks with memory. - `gpu/vk/vk_vma.cpp`: a sparse buffer, and its blocks' memory from VMA (counted in its budget). `vmaFreeMemory` joins the recorder's drain boundaries (`gpu/vk/vk_recorder.h`). - `gpu/vk/vk_memory.cpp`: - `mirror_make`: a sparse buffer with memory for the blocks a game pool is in. The page table points blocks with none at guest memory, as for a pool with no mirror. - `gpu_buffer_bind`: a binding that reaches a block with no memory is served from guest memory for that submission, and the block is wanted. - `sparse_grow`: at each submission's start, the wanted blocks and the blocks newly in a pool get memory in one batched bind, filled as a new mirror is. - `sparse_release`: a block no pool or binding has reached for 600 submissions gives its memory back, once the GPU is past it. - Binds signal a timeline semaphore that the recordings wait for on the GPU, not on the CPU (`RX_VK_SPARSE_WAIT=1`: on the CPU, as before). - Write-backs and a retired mirror's inheritance are clipped to blocks with memory. - `gpu/vk/vk_replay.cpp`: submissions of recordings wait for the binds' semaphore. - Tests: `tests/test_sparse_mirror.py` (+ `tests/test_sparse_mirror.cpp`). `test_direct_buffer_writes` and `test_readback_part` were updated for the new code they slice. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_MIRROR_SPARSE` | `=1`: sparse VRAM mirrors, memory only in the blocks a game pool is in (needs strict sparse residency for buffers; off otherwise, logged) | off | off by default; on after AMD/Intel testing | cards near their VRAM limit (6-8 GB) | cards with plenty of VRAM (no fps change at 12 GB) | AMD (RADV, Windows), Intel, Linux | | `RX_VK_SPARSE_WAIT` | `=1`: each sparse bind waited for on the CPU, as before | off | off | — | — | — | Documented in `docs/development.md`'s switch reference. ## Measurements **WIP: the Longshot A/B (main @ 2e1e2e8c against this PR with `RX_VK_MIRROR_SPARSE=1`) is queued.** The numbers below are from Hookshot: the same code, one build, the switch off and on. Baseline: `Hookshot perf/sparse-mirror @ 130ee5e6, RX_VK_MIRROR_SPARSE unset` Why not main: measured on Hookshot before the move. Hardware: RTX 4070 SUPER 12 GB, NVIDIA 617.42, i7-14700K, 32 GB, Windows 11 Pro 26200 (6 and 8 GB cards simulated with `RX_VK_TEST_ALLOWED_MB`) Scene: Hyrule Field walk, Castle Town to the field, Kokiri Forest; 2560x1440 DLAA, unattended Method: frame trace and VRAM log lines, three pairs per scene where stated | | baseline (unset) | `RX_VK_MIRROR_SPARSE=1` | |---|---|---| | 6 GB allowance (`RX_VK_TEST_ALLOWED_MB=6144`, `RX_VK_MIRROR_MB=1536`), field walk, three pairs: fps | 47.7, 51.5, 49.9 | 57.3, 55.3, 58.2 | | the same: frames over 25 ms | 3,042, 1,840, 2,114 | 625, 1,160, 408 | | the same: VRAM peak | 7,327, 6,254, 6,444 MB | 6,394, 6,200, 6,387 MB | | 8 GB allowance (`=6900`), field walk: fps / VRAM peak | 59.0-59.1 / 7,028-7,138 MB | 59.1-59.3 / 6,676-7,018 MB | | Castle Town to the field: the load's longest frame | 816-902 ms | 866-961 ms | | Kokiri Forest: VRAM mean | 8,215 MB | 7,462-7,507 MB | **A real 8 GB card** (klikso, on NotRexed/Hookshot#2794): RTX 3060 Ti 8 GB, 2560x1440 DLSS K. About 430 MB less in the mirrors, 120-370 MB less VRAM in Castle Town Market, and a higher texture budget. Frame rate and long frames were the same within three pairs; nothing broke. ## Testing **Tested:** - The game-free checks that read the changed files: 109 pass. 8 fail, and those same 8 fail on Longshot main on this PC (checked with the same files touched): `test_audio_volume`, `test_oot_fov`, `test_opening_skip`, `test_present_aspect_vulkan`, `test_present_thread`, `test_readback_wait_bounded`, `test_text_size`, `test_texture_staging_publication`. They don't build with this PC's compilers. - In the game: - the runs above; - screenshots of Castle Town, Kokiri and the field look the same on and off; - no device lost and no write-back warning in any run; - the player's own sessions with it on, in Hookshot builds since 12 Nov. - Two independent code reviews of earlier versions found ten problems, all fixed. **Not tested:** - AMD (RADV and Windows), Intel, Linux. - Long sessions on a real 6 GB card. - Longshot builds of this exact commit (queued). **Needs other testers:** - AMD RADV (Linux), a 6-8 GB card: the field walk on and off. Expect lower VRAM, equal or higher fps, and no lost device (request to open). - AMD on Windows: the same (request to open). - Intel Arc: the same, or the log line saying it's off when the device lacks strict sparse residency (request to open). ## Screenshots Same spot, off and on: identical. To add from the Longshot A/B. ## Risk and rollback Off by default. When on, a block that is wanted but not bound yet is served from guest memory for one submission, as a pool with no mirror always is. A driver bug in sparse binding could lose the device. Unset `RX_VK_MIRROR_SPARSE` to turn it off. ## Reviewer notes Start with `gpu/vk/sparse_mirror.h`, then `sparse_grow` and `sparse_release` in `vk_memory.cpp`. ## Issues See #130 ## 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. 8 fail here and fail on main here too (named above). The full run is still to do. - [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). - [x] A row in `docs/tested-configurations.md` for each measured run (the Hookshot runs; Longshot rows to add with the A/B). - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
perf(vk): sparse VRAM mirrors, memory only where a game pool is (RX_VK_MIRROR_SPARSE=1)
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 19s
Website / Website on Cloudflare (pull_request_target) Has been skipped
8cf52d3f44
Ported from Hookshot perf/sparse-mirror (990d477d, NotRexed/Hookshot#2794) onto Longshot
main as one change. Off by default.

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:45:34 +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 19s
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/sparse-mirror:GummiNaomi-perf/sparse-mirror'
git switch 'GummiNaomi-perf/sparse-mirror'
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!250
No description provided.