WIP: perf: Hookshot's new defaults and speed-ups merged after Longshot branched, restored #288

Draft
TrashPanda wants to merge 26 commits from TrashPanda/Longshot:restore/defaults-after-rc4 into main
Contributor

Why

Hookshot merged new defaults and speed-ups after Longshot branched: texture memory (BC1 for opaque ASTC, a Texture
detail row, 32 MB image blocks, the 7/8 trim), barrier-ordered passes on NVIDIA, staging and view caches, host hot-path
costs, ReplayPin on Linux, pipeline lists and Auto run. They change defaults for players, so they wait for after rc.4,
or for Longshot numbers. Split out of #209.

What changed

Split out of #209 as its reviewer asked (review on #209, point 4, group e). Each commit is the merged Hookshot pull request under its original author, carried through Longshot's rename; a Restored-from: trailer names it, and the bare Hookshot numbers in the messages and the restored text are now full URLs.

Credit to the authors of the restored work: Dewey Harry D. bartolo, getzabelz, NotRexed, Mooki, David Sandner, FIocker, Maksim Klimovich, Erfboom, Juliann, jarynclouatre, yassine milal, klikso, RufflezAU.

  • textures and memory: opaque ASTC as BC1; a Texture detail row (Auto, Full, Medium, Low); 32 MB image pool blocks; a
    large excess cut to 7/8 again; render targets covered by newer ones let go; no pool import for one unswizzle; pool
    import without a heap walk; udmabuf fallback; staging tail suballocation
  • barriers: back-to-back passes and dispatches ordered by the game's barriers, on NVIDIA by default
    (https://github.com/NotRexed/Hookshot/pull/2559). This is a new default for every NVIDIA player with no Longshot
    measurement yet
    : dispatches back to back get no barrier of their own, so a hazard the game's barriers miss would
    show as flicker or wrong pixels. It needs a measurement on Longshot (and a look for artefacts) before it merges;
    RX_VK_GAME_ORDER=0 turns it off
  • texture views: a front cache for repeated lookups (https://github.com/NotRexed/Hookshot/pull/2960). The staging
    prewarm half of that PR is left out
    : the four 64 MB arenas it made in replay_start went through texture_frame's
    keep and idle rules, so on a card with 6.5 GB or less three were freed at the first frame and all of them 30 s later,
    for 256 MB of host-visible memory at start. Keeping them needs an exemption on Arena and a check of its own; that
    can follow separately
  • host: own-thread lookups, a condition-variable signal with no waiter, switches read once
  • shaders: narrow global stores; the noperspective workaround baked offline; mesh picks cached per thread
  • Linux: ReplayPin on the reserved core
  • statistics: native shader keys, GPU resource waits; one pipeline play file a session
  • release: two shipped pipeline lists
  • input: Auto run, as option 56 (HOS_OPT_AUTO_RUN): upstream it was 55, which Longshot's Damage taken row (#138)
    now has. An option number, not a change to MkwVkHost or MkwVkRenderer
  • docs: the new switches in the switch reference

Split siblings: tools, checks and docs #284, test-request pinning #285, rc.4 player fixes #286, renderer fixes #287.

Commits, in order:

  • 4ec9a872 build(release): a shipped pipeline list from a player's progress on Linux AMD, 1364 pipelines (Hookshot 3029, by Dewey Harry D. bartolo)
  • 8dadb975 build(release): a shipped pipeline list from an overnight tour of 21 areas, 3388 pipelines (Hookshot 3030, by Dewey Harry D. bartolo)
  • bba74c19 feat(input): Auto run, the sprint goes on after A is let go (Hookshot 2747, by getzabelz)
  • ea98c37a fix(vk): one pipeline play file a session, and the next start puts them together (Hookshot 2625, by NotRexed)
  • 794f067b fix(vk): report GPU resource waits beyond recording-slot reuse (Hookshot 2897, by Mooki)
  • 53d4ed32 fix(linux): enable ReplayPin on the reserved core and reset child thread affinity (Hookshot 2903, by David Sandner)
  • 5404f36f fix(shaders): the noperspective workaround baked offline too; RX_VK_LOWER_SKIP in the program stamp (Hookshot 2782, by FIocker)
  • 21ae09b1 perf(vk): opaque ASTC textures kept as BC1, half the video memory of BC3 (Hookshot 2554, by NotRexed)
  • e4d17e98 feat(menu): a Texture detail row (Auto, Full, Medium, Low) for cards short of video memory (Hookshot 2557, by NotRexed)
  • 01608e79 perf(vk): back-to-back passes and dispatches ordered by the game's barriers (NVIDIA by default) (Hookshot 2559, by NotRexed)
  • ccfb04b6 fix(vk): report native shader keys in draw statistics (Hookshot 2896, by Maksim Klimovich)
  • 43b799c7 perf(vk): suballocate untouched tails of in-flight staging arenas (Hookshot 2469, by Erfboom)
  • cffd08e2 perf(vk): cache mesh shader picks in thread-local program caches (Hookshot 2476, by Erfboom)
  • ea6dc5f5 fix(vk): fall back to host pointers on udmabuf rejection and retry mirror allocation (Hookshot 2482, by Erfboom)
  • dcd63673 perf(host): three per-call costs on hot paths removed (own-thread lookups, a Signal with no waiter, switches read once) (Hookshot 2708, by Maksim Klimovich)
  • 6ca3c01c fix(vk): let render targets go as newer ones together cover them, and reuse their images at once (Hookshot 2766, by Juliann)
  • 4fda7ba9 perf(vk): no pool import for one GPU unswizzle, and the CPU unswizzle threaded from 4 MB (Hookshot 2785, by FIocker)
  • 117992f8 perf(vk): a pool import does not pay a walk over the game's heap (Hookshot 3043, by jarynclouatre)
  • 98edff67 fix(vk): image pool blocks of 32 MB by default, not 128 (Hookshot 2967, by yassine milal)
  • df66d827 fix(vk): a large excess over the 90% rule or the driver's limit is cut to 7/8 again, only a small one is trimmed (Hookshot 3020, by klikso)
  • b3e80733 fix(vk): answer repeated texture views from a front cache (Hookshot 2960, by RufflezAU)
  • 2fb964ea fix: write 8/16-bit global stores directly instead of a compare-exchange loop (Hookshot 2976, by yassine milal)
  • f9f3eeda test: ViewFast stand-ins in the checks that cut draw_begin_recording (mine)
  • b543f0b4 docs: the restored Hookshot references as full URLs (mine)
  • 9fe72e19 docs(development): the new renderer and host switches of the restored speed-ups (mine)
  • 5ce3b68a test(vk): the occlusion wiring check names StatsQuery's arguments as the draw statistics now pass them (mine)

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
Texture detail row (Insert menu) Full, Medium or Low texture size for cards short of video memory Auto Auto 4 to 6 GB cards Cards with room Longshot builds
Auto run row (Controls) The sprint goes on after A is let go Off Off - - Longshot builds
RX_VK_ASTC_BC1=0 All ASTC as BC3, as before Opaque ASTC as BC1 On after a Longshot run Low-VRAM cards - Longshot builds
RX_VK_GAME_ORDER=1 / =0 The game's barriers order back-to-back passes and dispatches On for NVIDIA Needs a Longshot measurement first NVIDIA - AMD, Intel; Longshot builds
RX_VK_IMAGE_BLOCK_MB=<n> Existing; image pool block size 32 (was 128) On after a Longshot run Low-VRAM cards - Longshot builds
RX_VK_VIEW_FAST=0 No texture-view front cache On Default CPU-bound scenes - Longshot builds
RX_VK_NATIVE_NARROW=0 8/16-bit global stores through the compare-exchange loop, as before Direct stores On after a Longshot run - - Longshot builds
RX_VK_IMPORT_FENCE=0 No fence page while the driver checks an imported pointer On Default - - -
RX_VK_UNSWIZZLE_IMPORT=1 A single GPU unswizzle imports the pool again, as before Off Default - - -
RX_VK_LOWER_SKIP=<key prefix,...> Existing; now part of the program stamp Off Diagnostics only - - -
HOS_CV_SIGNAL_ALWAYS=1 Every condition-variable signal takes the slow path, as before Off Default - - -

Measurements

No Longshot numbers yet: each change was measured upstream on Hookshot's tree (its commit message and tested-configuration
rows). These are new defaults, so this PR needs Longshot numbers before it goes ready; until then the game-free checks and
builds below, main and this PR on the same PC.

main: d9648863 (2026-11-13)
this PR: restore/defaults-after-rc4 @ 6c477f22 (the runs; now 5ce3b68a, after the docs-only rebase onto main's 514591a3)
Hardware: RTX 4070 Ti (driver 595.99.02), Ryzen 7 9800X3D, Ubuntu 26.04.1
Scene: none (no game run)
Method: python3 -I -B tests/run_all.py --jobs 6, once on each; the builds below
How to reproduce: the same command on each commit

main @ d9648863 this PR @ 6c477f22
run_all.py: passed / failed / skipped 513/2/18 523/2/18
failing test_lift_cache.py, test_menu_i18n.py test_lift_cache.py, test_menu_i18n.py
no-game build, Linux: files / compile failures / undefined at link 406 / 0 / 18 406 / 0 / 18 (the same symbols)
no-game build, Windows (llvm-mingw): files / compile failures / undefined at link 405 / 0 / 19 405 / 0 / 19 (the same symbols)

The numbers still hold on 514591a3: main's #274 changed a Discord link in 11 docs files and nothing these checks or builds compile; the checks that read those docs were run again on this head.

Testing

Tested: python3 -I -B tests/run_all.py --jobs 6 on Linux (Ubuntu 26.04.1, Python 3.14, Vulkan and SPIR-V headers found): this PR @ 6c477f22: 523 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py); main @ d9648863 on the same PC: 513 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py). test_lift_cache fails on main the same way (the lift's scripts do not import here), and test_menu_i18n fails on that main too. Main then gained 514591a3 (#274, a Discord link in 11 docs files only); after the rebase onto it, the checks that read those docs passed again at 5ce3b68a.

Built at 6c477f22 with no game, on Linux (system clang 21, lld) and for Windows (cross-built here with tools.lock's llvm-mingw 20261104 and its windows-x64 CPM cache, which has FFmpeg and dav1d): (before the docs-only rebase onto #274) nx2/build.py --standalone with an empty lift compiled everything in the repository that the game links, the host, its libraries and the Vulkan back end (406 files on Linux, 405 for Windows), with no failure, and its link stopped only at the symbols the game's lifted code provides (18 on Linux, 19 on Windows: the same list as main @ d9648863). RX_VK_ONLY=1 RX_RELEASE=1 linked longshot_vk.so and longshot_vk.dll, and builder/setup/setup.c linked as LongshotBuilder-Setup.exe with build-windows.yml's command. The five split branches merged together locally (the API version taken as 23) compiled and linked the same way, so they also build together.

Not tested: a run in the game, and any measurement on Longshot: the game build needs the game's lifted code.

Needs other testers:

  • NVIDIA, Windows or Linux: main and this PR in Kokiri Forest and Hyrule Field, RX_VK_GAME_ORDER=1 against =0, two
    interleaved runs each: GPU time and fps, and anything drawn wrong (T-Y5MJ, claimable once a maintainer adds the testing wanted label)
  • A 4 to 6 GB card: VRAM use and fps in Hyrule Field, main against this PR (T-Y5MJ)

Screenshots

None: no run in the game yet (the Texture detail and Auto run rows show on screen).

Risk and rollback

Every player's defaults change (NVIDIA's barriers, texture formats and memory). Each has a switch above to turn it off,
and each commit reverts on its own.

Reviewer notes

Please merge with rebase, not squash, so that each author keeps their commit. One commit of mine adds ViewFast
stand-ins in the three checks that cut draw_begin_recording. Two conflicts with main were resolved keeping main's
code: in build_descriptors the front cache's timers (https://github.com/NotRexed/Hookshot/pull/2960) sit inside
#107's handle-table branch, and the draw statistics' StatsQuery(cmd, p, d)
(https://github.com/NotRexed/Hookshot/pull/2896) keeps #260's OccGuard after it. https://github.com/NotRexed/Hookshot/pull/2559 could also
move to its own PR if you'd rather let the rest go first.

Issues

See #209

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 (any failure that also fails on main is named above).
  • 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).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (no Longshot numbers yet: see Measurements; WIP)
  • 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

  • Screenshots for a change that shows on screen. (None yet: no run in the game.)
  • 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 restored rows, from the upstream runs.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Compiled with no game; see Testing.)
  • Docs updated.
## Why Hookshot merged new defaults and speed-ups after Longshot branched: texture memory (BC1 for opaque ASTC, a Texture detail row, 32 MB image blocks, the 7/8 trim), barrier-ordered passes on NVIDIA, staging and view caches, host hot-path costs, ReplayPin on Linux, pipeline lists and Auto run. They change defaults for players, so they wait for after rc.4, or for Longshot numbers. Split out of [#209](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/209). ## What changed Split out of [#209](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/209) as its reviewer asked (review on #209, point 4, group e). Each commit is the merged Hookshot pull request under its original author, carried through Longshot's rename; a `Restored-from:` trailer names it, and the bare Hookshot numbers in the messages and the restored text are now full URLs. Credit to the authors of the restored work: Dewey Harry D. bartolo, getzabelz, NotRexed, Mooki, David Sandner, FIocker, Maksim Klimovich, Erfboom, Juliann, jarynclouatre, yassine milal, klikso, RufflezAU. - textures and memory: opaque ASTC as BC1; a Texture detail row (Auto, Full, Medium, Low); 32 MB image pool blocks; a large excess cut to 7/8 again; render targets covered by newer ones let go; no pool import for one unswizzle; pool import without a heap walk; udmabuf fallback; staging tail suballocation - barriers: back-to-back passes and dispatches ordered by the game's barriers, on NVIDIA by default (https://github.com/NotRexed/Hookshot/pull/2559). **This is a new default for every NVIDIA player with no Longshot measurement yet**: dispatches back to back get no barrier of their own, so a hazard the game's barriers miss would show as flicker or wrong pixels. It needs a measurement on Longshot (and a look for artefacts) before it merges; `RX_VK_GAME_ORDER=0` turns it off - texture views: a front cache for repeated lookups (https://github.com/NotRexed/Hookshot/pull/2960). **The staging prewarm half of that PR is left out**: the four 64 MB arenas it made in `replay_start` went through `texture_frame`'s keep and idle rules, so on a card with 6.5 GB or less three were freed at the first frame and all of them 30 s later, for 256 MB of host-visible memory at start. Keeping them needs an exemption on `Arena` and a check of its own; that can follow separately - host: own-thread lookups, a condition-variable signal with no waiter, switches read once - shaders: narrow global stores; the noperspective workaround baked offline; mesh picks cached per thread - Linux: ReplayPin on the reserved core - statistics: native shader keys, GPU resource waits; one pipeline play file a session - release: two shipped pipeline lists - input: Auto run, as option 56 (`HOS_OPT_AUTO_RUN`): upstream it was 55, which Longshot's Damage taken row (#138) now has. An option number, not a change to `MkwVkHost` or `MkwVkRenderer` - docs: the new switches in the switch reference Split siblings: tools, checks and docs [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284), test-request pinning [#285](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/285), rc.4 player fixes [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286), renderer fixes [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287). Commits, in order: - `4ec9a872` build(release): a shipped pipeline list from a player's progress on Linux AMD, 1364 pipelines ([Hookshot 3029](https://github.com/NotRexed/Hookshot/pull/3029), by Dewey Harry D. bartolo) - `8dadb975` build(release): a shipped pipeline list from an overnight tour of 21 areas, 3388 pipelines ([Hookshot 3030](https://github.com/NotRexed/Hookshot/pull/3030), by Dewey Harry D. bartolo) - `bba74c19` feat(input): Auto run, the sprint goes on after A is let go ([Hookshot 2747](https://github.com/NotRexed/Hookshot/pull/2747), by getzabelz) - `ea98c37a` fix(vk): one pipeline play file a session, and the next start puts them together ([Hookshot 2625](https://github.com/NotRexed/Hookshot/pull/2625), by NotRexed) - `794f067b` fix(vk): report GPU resource waits beyond recording-slot reuse ([Hookshot 2897](https://github.com/NotRexed/Hookshot/pull/2897), by Mooki) - `53d4ed32` fix(linux): enable ReplayPin on the reserved core and reset child thread affinity ([Hookshot 2903](https://github.com/NotRexed/Hookshot/pull/2903), by David Sandner) - `5404f36f` fix(shaders): the noperspective workaround baked offline too; RX_VK_LOWER_SKIP in the program stamp ([Hookshot 2782](https://github.com/NotRexed/Hookshot/pull/2782), by FIocker) - `21ae09b1` perf(vk): opaque ASTC textures kept as BC1, half the video memory of BC3 ([Hookshot 2554](https://github.com/NotRexed/Hookshot/pull/2554), by NotRexed) - `e4d17e98` feat(menu): a Texture detail row (Auto, Full, Medium, Low) for cards short of video memory ([Hookshot 2557](https://github.com/NotRexed/Hookshot/pull/2557), by NotRexed) - `01608e79` perf(vk): back-to-back passes and dispatches ordered by the game's barriers (NVIDIA by default) ([Hookshot 2559](https://github.com/NotRexed/Hookshot/pull/2559), by NotRexed) - `ccfb04b6` fix(vk): report native shader keys in draw statistics ([Hookshot 2896](https://github.com/NotRexed/Hookshot/pull/2896), by Maksim Klimovich) - `43b799c7` perf(vk): suballocate untouched tails of in-flight staging arenas ([Hookshot 2469](https://github.com/NotRexed/Hookshot/pull/2469), by Erfboom) - `cffd08e2` perf(vk): cache mesh shader picks in thread-local program caches ([Hookshot 2476](https://github.com/NotRexed/Hookshot/pull/2476), by Erfboom) - `ea6dc5f5` fix(vk): fall back to host pointers on udmabuf rejection and retry mirror allocation ([Hookshot 2482](https://github.com/NotRexed/Hookshot/pull/2482), by Erfboom) - `dcd63673` perf(host): three per-call costs on hot paths removed (own-thread lookups, a Signal with no waiter, switches read once) ([Hookshot 2708](https://github.com/NotRexed/Hookshot/pull/2708), by Maksim Klimovich) - `6ca3c01c` fix(vk): let render targets go as newer ones together cover them, and reuse their images at once ([Hookshot 2766](https://github.com/NotRexed/Hookshot/pull/2766), by Juliann) - `4fda7ba9` perf(vk): no pool import for one GPU unswizzle, and the CPU unswizzle threaded from 4 MB ([Hookshot 2785](https://github.com/NotRexed/Hookshot/pull/2785), by FIocker) - `117992f8` perf(vk): a pool import does not pay a walk over the game's heap ([Hookshot 3043](https://github.com/NotRexed/Hookshot/pull/3043), by jarynclouatre) - `98edff67` fix(vk): image pool blocks of 32 MB by default, not 128 ([Hookshot 2967](https://github.com/NotRexed/Hookshot/pull/2967), by yassine milal) - `df66d827` fix(vk): a large excess over the 90% rule or the driver's limit is cut to 7/8 again, only a small one is trimmed ([Hookshot 3020](https://github.com/NotRexed/Hookshot/pull/3020), by klikso) - `b3e80733` fix(vk): answer repeated texture views from a front cache ([Hookshot 2960](https://github.com/NotRexed/Hookshot/pull/2960), by RufflezAU) - `2fb964ea` fix: write 8/16-bit global stores directly instead of a compare-exchange loop ([Hookshot 2976](https://github.com/NotRexed/Hookshot/pull/2976), by yassine milal) - `f9f3eeda` test: ViewFast stand-ins in the checks that cut draw_begin_recording (mine) - `b543f0b4` docs: the restored Hookshot references as full URLs (mine) - `9fe72e19` docs(development): the new renderer and host switches of the restored speed-ups (mine) - `5ce3b68a` test(vk): the occlusion wiring check names StatsQuery's arguments as the draw statistics now pass them (mine) ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | Texture detail row (Insert menu) | Full, Medium or Low texture size for cards short of video memory | Auto | Auto | 4 to 6 GB cards | Cards with room | Longshot builds | | Auto run row (Controls) | The sprint goes on after A is let go | Off | Off | - | - | Longshot builds | | `RX_VK_ASTC_BC1=0` | All ASTC as BC3, as before | Opaque ASTC as BC1 | On after a Longshot run | Low-VRAM cards | - | Longshot builds | | `RX_VK_GAME_ORDER=1` / `=0` | The game's barriers order back-to-back passes and dispatches | On for NVIDIA | **Needs a Longshot measurement first** | NVIDIA | - | AMD, Intel; Longshot builds | | `RX_VK_IMAGE_BLOCK_MB=<n>` | Existing; image pool block size | 32 (was 128) | On after a Longshot run | Low-VRAM cards | - | Longshot builds | | `RX_VK_VIEW_FAST=0` | No texture-view front cache | On | Default | CPU-bound scenes | - | Longshot builds | | `RX_VK_NATIVE_NARROW=0` | 8/16-bit global stores through the compare-exchange loop, as before | Direct stores | On after a Longshot run | - | - | Longshot builds | | `RX_VK_IMPORT_FENCE=0` | No fence page while the driver checks an imported pointer | On | Default | - | - | - | | `RX_VK_UNSWIZZLE_IMPORT=1` | A single GPU unswizzle imports the pool again, as before | Off | Default | - | - | - | | `RX_VK_LOWER_SKIP=<key prefix,...>` | Existing; now part of the program stamp | Off | Diagnostics only | - | - | - | | `HOS_CV_SIGNAL_ALWAYS=1` | Every condition-variable signal takes the slow path, as before | Off | Default | - | - | - | ## Measurements No Longshot numbers yet: each change was measured upstream on Hookshot's tree (its commit message and tested-configuration rows). These are new defaults, so this PR needs Longshot numbers before it goes ready; until then the game-free checks and builds below, main and this PR on the same PC. main: d9648863 (2026-11-13) this PR: restore/defaults-after-rc4 @ 6c477f22 (the runs; now 5ce3b68a, after the docs-only rebase onto main's 514591a3) Hardware: RTX 4070 Ti (driver 595.99.02), Ryzen 7 9800X3D, Ubuntu 26.04.1 Scene: none (no game run) Method: `python3 -I -B tests/run_all.py --jobs 6`, once on each; the builds below How to reproduce: the same command on each commit | | main @ d9648863 | this PR @ 6c477f22 | |---|---|---| | `run_all.py`: passed / failed / skipped | 513/2/18 | 523/2/18 | | failing | test_lift_cache.py, test_menu_i18n.py | test_lift_cache.py, test_menu_i18n.py | | no-game build, Linux: files / compile failures / undefined at link | 406 / 0 / 18 | 406 / 0 / 18 (the same symbols) | | no-game build, Windows (llvm-mingw): files / compile failures / undefined at link | 405 / 0 / 19 | 405 / 0 / 19 (the same symbols) | The numbers still hold on `514591a3`: main's #274 changed a Discord link in 11 docs files and nothing these checks or builds compile; the checks that read those docs were run again on this head. ## Testing **Tested:** `python3 -I -B tests/run_all.py --jobs 6` on Linux (Ubuntu 26.04.1, Python 3.14, Vulkan and SPIR-V headers found): this PR @ `6c477f22`: 523 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py); main @ `d9648863` on the same PC: 513 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py). `test_lift_cache` fails on main the same way (the lift's scripts do not import here), and `test_menu_i18n` fails on that main too. Main then gained `514591a3` (#274, a Discord link in 11 docs files only); after the rebase onto it, the checks that read those docs passed again at `5ce3b68a`. **Built** at `6c477f22` with no game, on Linux (system clang 21, lld) and for Windows (cross-built here with tools.lock's llvm-mingw 20261104 and its windows-x64 CPM cache, which has FFmpeg and dav1d): (before the docs-only rebase onto #274) `nx2/build.py --standalone` with an empty lift compiled everything in the repository that the game links, the host, its libraries and the Vulkan back end (406 files on Linux, 405 for Windows), with no failure, and its link stopped only at the symbols the game's lifted code provides (18 on Linux, 19 on Windows: the same list as main @ `d9648863`). `RX_VK_ONLY=1 RX_RELEASE=1` linked `longshot_vk.so` and `longshot_vk.dll`, and `builder/setup/setup.c` linked as `LongshotBuilder-Setup.exe` with build-windows.yml's command. The five split branches merged together locally (the API version taken as 23) compiled and linked the same way, so they also build together. **Not tested:** a run in the game, and any measurement on Longshot: the game build needs the game's lifted code. **Needs other testers:** - NVIDIA, Windows or Linux: main and this PR in Kokiri Forest and Hyrule Field, `RX_VK_GAME_ORDER=1` against `=0`, two interleaved runs each: GPU time and fps, and anything drawn wrong (T-Y5MJ, claimable once a maintainer adds the `testing wanted` label) - A 4 to 6 GB card: VRAM use and fps in Hyrule Field, main against this PR (T-Y5MJ) ## Screenshots None: no run in the game yet (the Texture detail and Auto run rows show on screen). ## Risk and rollback Every player's defaults change (NVIDIA's barriers, texture formats and memory). Each has a switch above to turn it off, and each commit reverts on its own. ## Reviewer notes Please merge with **rebase**, not squash, so that each author keeps their commit. One commit of mine adds `ViewFast` stand-ins in the three checks that cut `draw_begin_recording`. Two conflicts with main were resolved keeping main's code: in `build_descriptors` the front cache's timers (https://github.com/NotRexed/Hookshot/pull/2960) sit inside #107's handle-table branch, and the draw statistics' `StatsQuery(cmd, p, d)` (https://github.com/NotRexed/Hookshot/pull/2896) keeps #260's `OccGuard` after it. https://github.com/NotRexed/Hookshot/pull/2559 could also move to its own PR if you'd rather let the rest go first. ## Issues See #209 ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [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). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (no Longshot numbers yet: see Measurements; WIP) - [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 - [ ] Screenshots for a change that shows on screen. (None yet: no run in the game.) - [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 restored rows, from the upstream runs.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Compiled with no game; see Testing.) - [x] Docs updated.
A list from my own progress on Linux: Kokiri Forest, Inside the Deku
Tree, Hyrule Field and the road toward Kakariko.

contrib/deuxque-linux-rx6600-play holds 1364 pipelines (graphics 1198,
mesh 166) of 879 programs, version 2. add took only what the shipped
sources did not have yet (973 of them are not in any list before it),
and nx2/pipeline_states.py check passes.

Recorded on build v2.1.0-258-g51da0464 (main at 51da0464), AMD Radeon RX
6600, RADV on Mesa 26.2, Linux, at 1080p with no upscaler and no mods.
Settings in the note: RX_VK_PARALLEL=4, RX_VK_PARALLEL_SEG=32,
RX_VK_COMPACT=0, mesh shaders on the compute fallback.

Adds the .bin.xz and its note, and nothing else.

Co-authored-by: Dewey Harry D. bartolo <>
Restored-from: https://github.com/NotRexed/Hookshot/pull/3029
A list from a scripted overnight tour through 21 areas of my saves,
played through the game's control bridge: Hyrule Field, Kokiri Forest,
the Lost Woods, Sacred Forest Meadow, Inside the Deku Tree, Dodongo's
Cavern, Inside Jabu-Jabu, Zora's Domain, Goron City, Death Mountain
Trail, the Graveyard, the Bottom of the Well, Gerudo Training Ground,
Haunted Wasteland, Desert Colossus, the Forest, Dodongo and Fire
Temples, and Inside Ganon's Castle — as the child, at four times of day,
walking, rolling and jump attacking at each stop.

contrib/deuxque-linux-rx6600-tour holds 3388 pipelines (graphics 3111,
mesh 277) of 1951 programs, version 2; 2456 of them are not in any list
before it. nx2/pipeline_states.py check passes.

Recorded on build v2.1.0-rc.3-256-g51da0464, AMD Radeon RX 6600, RADV on
Mesa 26.2, Linux, 1080p, no upscaler, no mods. Settings in the note:
RX_VK_PARALLEL=4, RX_VK_PARALLEL_SEG=32, RX_VK_COMPACT=0, mesh shaders
on the compute fallback.

Adds the .bin.xz and its note, and nothing else.

Co-authored-by: Dewey Harry D. bartolo <>
Restored-from: https://github.com/NotRexed/Hookshot/pull/3030
**What this is for:** sprinting needs A held the whole time. **Auto
run** (Controls tab, Off by default) lets the player hold A until Link
sprints, let go, and keep sprinting while the stick stays pushed. **What
is tested:** a Windows game run on an RX 6800 XT with an Xbox controller
over XInput (Xbox / PC layout), including swimming, ladders and climbing
walls, the keyboard alone (Shift as A, W to move), and a new game-free
test. **Not tested:** Epona (not reached yet in the test save), Linux,
other controllers.

## Problem

In this game A is the run button: pressed while moving, Link rolls, then
sprints for as long as A is held. Long runs across Hyrule Field mean
holding the bottom face button down the whole time.

## What changed

- **An Auto run row** on the Controls tab, after Stick dead zone: Off
(the default) or On. Help text: *"Sprint without holding A (or its key).
Hold it until Link sprints, then let go: he keeps going. Let go of the
stick or the movement keys, or tap A again, to stop."*
- With it on, A held for **0.4 s** while the left stick is pushed (past
30%) and then let go **stays held for the game**. A short press (a roll
on its own) is never kept.
- **It ends** when:
  - the stick comes back to the middle (Link stops);
- A is pressed again (the player's A takes over: letting go stops, a
long hold keeps the sprint again);
- Plus or Minus is pressed (the pause menu or the map opens with A let
go);
- Link is no longer the player's to move: an event or a talk playing,
the game paused, or the Insert menu open. So A is never held into a menu
entry or a text box.
- The stick and the other buttons pass untouched.
- **Code:**
  - `host/auto_run.h` holds the logic, game-free.
- `host/nn_hid.c` runs it in `npad_states` on each controller read,
after the flight and the free camera.
- `host/hos_main.c` gets `hos_oot_player_free()`: Link updated within
250 ms with no event playing, noted on his update in
`hos_oot_lift_player`.
- **Option id `HOS_OPT_AUTO_RUN = 55`** (`gpu/vk/vk_api.h`), answered in
`vk_replay.cpp`. I surveyed the open PRs that change `vk_api.h`: 52/53
(https://github.com/NotRexed/Hookshot/pull/2345), 52 (https://github.com/NotRexed/Hookshot/pull/2669, https://github.com/NotRexed/Hookshot/pull/2604, https://github.com/NotRexed/Hookshot/pull/2567), 54 (https://github.com/NotRexed/Hookshot/pull/2567, https://github.com/NotRexed/Hookshot/pull/2530), 60 (https://github.com/NotRexed/Hookshot/pull/2261) and 64
(https://github.com/NotRexed/Hookshot/pull/2607) are taken; 55 is free in main and in those heads.
- **Log line:** `hid: auto run: A kept held for the sprint` (once, the
first time it keeps A).
- **Docs:** a new row in the readme's menu table, and a note in its
Controls section.

![Auto run in the Insert menu](https://i.imgur.com/GEb8gRB.png)

## Testing

- **In game (Windows 11, RX 6800 XT, Xbox controller over XInput, Xbox /
PC layout):**
  - holding A until the sprint and letting go keeps Link sprinting;
  - letting go of the stick and tapping A stop it;
  - a short A press still only rolls;
- in the water, on ladders and climbing walls it behaves as expected
(nothing out of place);
- the keyboard: A bound to Shift, first with the controller's stick,
then moving with a key (W): kept and ended the same way as on the
controller.

The build tested in game had the same logic on an older main (8fcc8c22)
under the id 52 and the names `keep_running`/`run_lock.h`. This branch
is that change squashed on current main, renamed (`auto_run`) and moved
to id 55; it was not rebuilt and run again after that.
- **`tests/test_auto_run.py` (new):** plays presses through the header
and checks that the row, its answer, the call in `nn_hid.c` and the
readme are there. Cases:
  - kept after a long hold;
  - not kept after a short one;
  - A pressed again, Plus, an event;
  - the row off;
  - the stick under 30%, a diagonal;
  - A held while standing and then moving.
- **`tests/test_input_background.py`:** its fixture gets the header and
a `hos_oot_player_free` stub. Locally it fails to compile on main too
(`oc_consumed_pads set but not used` with llvm-mingw's clang
`-Wunused-but-set-global`), the same as
`tests/test_driver_cache_folder.py` (`mkdir`). Both are unrelated to
this change, so I'm leaving them to CI.

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2747

Ported to Longshot: the Auto run option is number 56. Longshot's Damage taken row (#138) took 55, the number this
had upstream.
**The bug (https://github.com/NotRexed/Hookshot/issues/2579).** Every save in play (every 32 new pipelines) wrote
its own `mkw_vk_pipelines_play_<n>.bin`. The files are folded into the
main pipeline cache only once a start finds 16 of them, so a long
session can leave more than that: the reporter had 22, and the owner's
test folder had 13.

**The fix** (`gpu/vk/vk_draw.cpp`):
- **During a session:** each save merges the generation it takes into
one session cache and writes that again, to one file with the session's
own number. A write that fails is kept and tried again at the next save,
even with nothing new.
- **At start:** the play files found are merged into pcache as before,
and also put together into one play file in their place. At 16 they are
still written into the main cache, as before.

So the folder holds at most the last start's combined file plus the
current session's. The 2.5 GB main cache is still not rewritten during
play.

**Tests:** `tests/test_pipeline_play_cache.py` and its C++ model are
updated:
- one file per session that grows with each save;
- a failed write retried at the next save;
- the start putting two play files together into one, numbered after the
highest.

All 9 cases pass, and `test_pipeline_cache_save` passes.

Fixes https://github.com/NotRexed/Hookshot/issues/2579

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2625
## Why
The old “GPU-bound wait” measured recording-slot reuse only: capture
late-page, readback and other resource waits could be invisible while it
read zero. This is a reporting fix, not a change to how the game runs.
Refs [#995](https://github.com/NotRexed/Hookshot/issues/995), where a
report today shows the old figure reading 0.0 ms on a GPU-limited card
([comment](https://github.com/NotRexed/Hookshot/issues/995#issuecomment-6828404093)).

## Change
- Keep the old value and label it “recording slots only”; add `host time
in GPU resource waits`, split into fences, timelines, idle calls and
blocking query reads.
- Measure the same native calls after the existing recorder boundary.
Nested waits count once; polls and Reflex pacing are excluded.
Scheduling, synchronization, rendering and settings are unchanged.
- This is elapsed host time summed across threads, not frame time, GPU
utilization or CPU execution. Descheduling contributes; do not add it to
the old value or containing phase timers.
- Recorder/CPU locks, display pacing/acquire, startup’s
cooperative-matrix probe and hidden submit/present blocking are outside
its scope.

## What was measured
Hyrule Field, RTX 4090/NVIDIA 615.78.08, i9-13900K, Arch Linux 7.2.15;
headless 5120×2880, render 100%, no frame generation, cap 240. Two
ordinary 95-second runs; complete log blocks in seconds 45–75:

| Build | Old slot value | New resource waits |
|---|---:|---:|
| main `51da0464` | 0.010 ms/frame | not recorded |
| counter `9d786cf1` (source `7acafacf`) | 0.000 ms/frame | 2.927
ms/frame |

The changed build reports **2.93 ms/frame** (2.916 in timelines) while
the old figure reads **0.0 ms**. Both runs ended at the time limit
without fatal/kernel faults. Competing CPU load differed (mean 21.94
versus 2.33 cores): this proves a reporting gap, not a speed gain or an
isolated GPU bottleneck.

## Checks and limits
- Rebased on `9b6072cd`, head `bfa7e18d` (the only conflict was the
shared results table; both sides' rows kept). Runtime patch changed
lines match old `203356e8`; only an additional test-fixture CRT define
was needed. The rebased runtime was **not rebuilt or run**.
- On the rebased head, `test_gpu_wait_stats.py` (50 checks),
`test_recorder_sync.py` and `test_reflex_swapchain_lifetime.py` pass
with GCC and Clang. Windows-target syntax passes for 3 captured
fixtures; later runtime-dependent variants and Windows execution are not
covered.
- Branch/title preflight passes; upstream advanced one commit to
`3c2d80cf3` during the checks and the branch still merges cleanly.
- Before-rebase negative control: the same accounting fixture executes
with main’s old macros and fails 9 of 50 checks on both compilers; it
does not merely fail to compile.
- Earlier full suite: 474 passed, 2 failed, 20 skipped. The missing
Reflex-fixture header was repaired; `test_fs_links.py` had GCC killed,
cause unproven. No new full-suite pass is claimed.
- **Not measured:** alternating instrumentation-overhead proof or the
CPU-limited control; no paired timing proof or speed claim. Two clock
reads and an atomic update around nonpolling waits have unmeasured
overhead.
- A full run of the project's checks in my fork, Windows included, is
green on these commits (the three new or changed tests run there on
Windows too). No Windows gameplay, AMD/Intel run, live Reflex/frame
generation, deliberate driver-failure or long-session proof.
- Main’s merged [#2683](https://github.com/NotRexed/Hookshot/pull/2683)
reports nested trace phases; this native-call sum has a different scope
and must not be added to them.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>

---------

Co-authored-by: GPT-6 Astra <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2897
### Problem

On Linux, `hos_reserved_cpus()` in `host/hos_thread.c` selects the last
performance core (updated for Intel hybrid CPUs in https://github.com/NotRexed/Hookshot/pull/2805) and
`hos_thread_pin()` strips that core (`may & ~reserved`) from all game
threads so it stays free for replay.

However, with default synchronous replay (`RX_VK_ASYNC=0`),
`replay_pin_mask()` in `gpu/vk/vk_replay.cpp` returned `0` on Linux
(`#if defined(_WIN32) ... #else return 0; #endif`, added in `ffeaed67`
because on Linux `pthread_create` inherits the calling thread's affinity
mask rather than the process mask, which trapped backend helper threads
spawned lazily on first use inside a replay onto the single reserved
core).

As a consequence on Linux:
1. The core reserved by `hos_reserved_cpus()` was stripped from all game
threads by `hos_thread_pin()`, yet **never used by `ReplayPin`**—leaving
the fastest performance core idle by default (which also explains why
https://github.com/NotRexed/Hookshot/pull/2805 observed no measurable frame-rate change in Hyrule Field on Linux
despite selecting a P-core).
2. Both `sead::MainThread` and `Presentation Thread` executed all
synchronous `submit_q` / `run_submit` / `present` work (`memory sync`,
`write-back diff`, `capture`, and replay) in the unreserved core pool
alongside the game's 11 heavy worker threads (`BaseProcExecutor 0–5` and
`ModuleSystemWorker 1–5`), causing frequent preemption during per-frame
memory sync and page capture.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2903

---

### What Changed

1. **`host/posix/win32_posix.c` (`apply_process_affinity`):**
- When a new thread registers in `win32_posix.c`—either via
`CreateThread` (`thread_entry`) or the first time a non-`CreateThread`
thread such as `std::thread` calls `GetCurrentThread()` in
`SetThreadDescription` / `SetThreadPriority` (`thread_self` when
`!self_thread`)—its affinity mask is initialized to `process_affinity`
(captured once before `main`).
- This matches Windows' behavior where newly created threads start with
the process affinity mask, ensuring that backend helper threads lazily
spawned on first use inside a pinned replay (`copy_helper_main`,
`par_worker_main`, `bg_main`, `load_worker`, `vk_jobs`) do not stay
trapped on the single reserved replay core. (Threads that explicitly pin
themselves afterwards, such as `mkw_vk replay` and `mkw_vk recorder`,
call `SetThreadAffinityMask` after `SetThreadDescription`, so their
explicit pin still applies.)
2. **`gpu/vk/vk_replay.cpp` (`replay_pin_mask`):**
- Enable `replay_pin_mask()` on Linux as well as Windows so `ReplayPin`
moves synchronous replay and capture sections onto
`host->reserved_cpus()` (and restores `replay_pin_prev` afterwards).
3. **`tests/test_reserved_core.py`:**
- Assert that `replay_pin_mask()` in `gpu/vk/vk_replay.cpp` is active on
Linux (not gated by `_WIN32`) and that `win32_posix.c`'s `thread_self`
and `thread_entry` reset new threads to `process_affinity`.

---

### Measured (`[vk-diag]` on Arch Linux, i5-13600KF + RX 9070 XT, RADV)

Sampled across **246,840 presents** (2,053 `[vk-diag]` blocks: 718
baseline blocks before vs. 1,335 blocks / 160,080 frames after isolating
synchronous replay/serial threads from `BaseProcExecutor` /
`ModuleSystemWorker` contention on P-cores):

| Metric (`[vk-diag]`, 600-present intervals) | Before (unpinned replay
on Linux) | After (isolated P-core replay) | Delta |
| :--- | :--- | :--- | :--- |
| **`memory sync` (median / avg)** | `2.23 ms` / `2.26 ms` | **`1.54 ms`
/ `1.67 ms`** | **-30.9%** (med) / **-26.1%** (avg) |
| **`memory sync` (p95)** | `3.48 ms` | **`3.00 ms`** | **-13.8%** |
| **`a capture taken: all` (median / avg)** | `0.31 ms` / `0.30 ms` |
**`0.19 ms` / `0.21 ms`** | **-38.7%** (med) / **-30.0%** (avg) |
| **`GPU wait for late pages` (median / avg)** | `0.30 ms` / `0.29 ms` |
**`0.18 ms` / `0.21 ms`** | **-40.0%** (med) / **-27.6%** (avg) |
| **`submit` replay total (median / avg)** | `6.37 ms` / `6.25 ms` |
**`5.27 ms` / `5.34 ms`** | **-17.3%** (med) / **-14.6%** (avg) |
| **Gameplay FPS (median, native before framegen)** | `64.3 FPS` |
**`69.5 FPS`** | **+5.2 FPS (+8.1%)** |

---

### Tests

- `python3 -I -B tests/test_reserved_core.py` — **PASS**
- `python3 -I -B tests/test_spin_by_role.py` — **PASS**
- `python3 -I -B tests/test_linux_saves_move.py` — **PASS**
- `gcc -fsyntax-only -Wall -Wextra -Werror -Ihost/posix
host/posix/win32_posix.c` — **PASS**
https://github.com/NotRexed/Hookshot/pull/1830 merged with a few non-blocking notes left from hckrman101's and
Joey-Magnus's reviews; this PR is those. The main one: an offline-baked
program bank didn't get the AMD noperspective workaround that live
translation now has.

## What it does

- **The workaround baked offline too** (`nx2/bake_ds/bake_ds.cpp`):
- The VS+FS path now translates the fragment stage before it emits the
vertex stage, as `vk_shader.cpp translate_into` does.
- It sets `noperspective_outputs` / `scaled_noperspective_inputs` the
same way: not with indexed fragment reads and not with transform
feedback.
- Without this, a bank baked offline kept the old interpolation, so the
red dotted line and particles on AMD (https://github.com/NotRexed/Hookshot/pull/1634, https://github.com/NotRexed/Hookshot/pull/1000) came back for
programs taken from it.
- **`RX_VK_LOWER_SKIP` is in the program stamp**: it's in
`translator_stamp`'s prefix list and in `bake_ds.cpp`'s, which
`test_program_stamp_knobs.py` keeps equal. Programs translated while
it's set are kept apart in the cache, so a bisection also sees programs
that were already cached. The comment says so.
- **One redundant term removed**: `DefineInputs`' `loads` is already
`info.loads.mask | info.passthrough.mask`, so FragCoord's condition is
just `loads.AnyComponent(PositionX) || noperspective_as_smooth`.
- **The "(super build)" tags** are gone from `emit_spirv.cpp`,
`stream.h` and `vk_shader.cpp`.

## Testing

- **Builds**: `vk_shader.cpp`, `spirv_emit_context.cpp` and
`emit_spirv.cpp` compile with clang using `nx2/build.py`'s renderer
flags. `bake_ds.cpp` is POSIX-only; I syntax-checked it on Windows with
stand-in `unistd.h`/`dlfcn.h`, so Linux CI is its real build.
- **Tests**: the stamp, bake and shader checks pass locally,
`test_program_stamp_knobs.py` among them. `test_bake_portability.py`
fails on this PC for main as well.
- **Screenshots** for https://github.com/NotRexed/Hookshot/pull/1830: omgitsgela's Steam Deck run there is the
before/after. If they're OK with it, their pair would go well in https://github.com/NotRexed/Hookshot/pull/1830's
description.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: FIocker <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2782
**Why.** On cards up to 8 GB, the big ASTC textures (terrain, buildings)
are decoded on the GPU and re-encoded as BC3, a byte per texel. A large
share of the game's textures have no transparency at all, and BC1 stores
them in half the space with the same colours. This targets the 8 GB "fps
fades over time" reports (https://github.com/NotRexed/Hookshot/pull/993): less VRAM for the same picture.

**How**
- **The choice** (`gpu/vk/astc_opaque.h`). A block counts as opaque only
when its header says so:
  - its colour endpoint mode has no alpha (0–3, 6–9, 11, 14, 15), or
  - it is an LDR void extent whose alpha high byte is 0xFF.
  
Everything uncertain counts as alpha and keeps BC3: alpha modes (4, 5,
10, 12, 13), partitions of different modes, and HDR void extents. Blocks
of zeros (storage the game hasn't written yet) decide nothing.
- **When** (`upload_astc_gpu`). A texture's first whole upload decides,
if every block with data in it is opaque and those blocks are most of
the storage. Its BC3 image is then made again as BC1 (`bc_reformat`).
The old image and views are released once the work that used them is
done, the same way the compaction moves images. Views are made again on
demand, and `destroy_gen` is bumped.
- **Switching back.** An upload into a BC1 image that has alpha in it
turns the image back into BC3 and uploads it whole.
- **Encoding.** `bc_encode_into` runs the existing encoder in its BC1
mode (`bc3 = 0`). Without texels under half alpha, it writes the same
four-colour blocks as BC3's colour half. A BC1 image's views are
`BC1_RGBA` (UNORM / SRGB).
- **Off switch.** `RX_VK_ASTC_BC1=0` keeps everything BC3. The log says
how many textures went BC1, how many went back, and the MB saved.

**Tests:** `tests/test_astc_opaque.py` builds blocks bit by bit (every
endpoint mode, shared and mixed partitions, void extents, zeros) and
checks the scan and the whole-upload rule. All tests that read
`vk_texture.cpp` pass, and the file compiles. The narrowing error at
line 1485 with plain clang flags is already on main.

**Not yet verified in-game:** VRAM in Hyrule Field with and without it,
plus the flash check. The PC is in use for the owner's 2.2.0 test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2554
**Why.** On 6 and 8 GB cards, VRAM fills over play time and the frame
rate fades (https://github.com/NotRexed/Hookshot/pull/993). Hookshot already drops the big textures' top mip
levels, but only on a driver budget of 4.5 GB or less at the Minimum
preset. Larger cards that run short couldn't choose it.

**What.** A **Texture detail** row in Graphics › Detail
(`opts.texture_detail`, ini `texture_detail`):

| Value | Big decoded ASTC textures |
|---|---|
| **Auto** (default) | as before: the low-VRAM rule on ≤4.5 GB at
Minimum, else the game's size |
| **Full** | the game's size (only the very largest still trimmed to
fit, as before) |
| **Medium** | largest level left out above 1024 texels, from 8 MB (the
low-VRAM rule) |
| **Low** | down to 512 texels, from 8 MB |

`texture_detail_dims` maps the row to the existing `max_dim` /
`skip_from` of the trimming in `create`. `RX_VK_TEX_MAX_DIM` and
`RX_VK_TEX_SKIP_MB` still override it. The setting applies to textures
as they are made, so a restart applies it to all of them. The readme's
option table lists the row.

**Tests**
- `tests/test_low_vram_texture_mips.py`: new cases Full on a small card,
Medium and Low on a large one, Auto on a large one, and Medium with the
dimension override. 29 configurations pass.
- `test_menu_sections`, `test_preset_exempt` and `test_option_ids` pass.
- `vk_texture.cpp` and `vk_menu.cpp` compile.

**Not yet verified in-game:** VRAM and the picture at Medium and Low.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2557
**Why.** Every draw pass begin and every compute dispatch records a full
barrier, which makes the GPU drain everything before each of them. The
game already records its own barrier (`nvnCommandBufferBarrier`, which
the replay keeps as a full barrier) wherever a pass samples what an
earlier one drew or a dispatch reads what another wrote. The extra
barriers between back-to-back work are pure GPU idle time: elsewhere it
measured 2.3 ms of a 9.7 ms GPU frame in Kokiri Forest, and about a
tenth off the GPU's frame time on an RTX 5090.

**What** (`RX_VK_GAME_ORDER`):
- **A draw pass right after another**, with nothing recorded between,
waits in its fragment stages only. This is the existing
`RX_VK_PASS_BARRIER=game` path, now on with the switch.
`RX_VK_PASS_BARRIER=game` / `full` still decide that part on their own.
- **A direct dispatch right after another** gets no barrier. "Right
after" means the same command buffer with no GPU work recorded between,
checked through the `work_epoch` count taken right after the previous
dispatch's command.
- **Still full barriers:** a switch between draws and compute, the
replay's own copies, clears, fills and uniform updates (all count as
work), indirect dispatches (their grid may come from the dispatch
before), a parallel replay, and the game's barriers.
- **Defaults:** on for NVIDIA (vendor 0x10de), since the game's barriers
were placed for an NVIDIA GPU. Off on other vendors, where it hasn't
run: `RX_VK_GAME_ORDER=1` to try it there, `=0` to turn it off
everywhere.
- **Logging:** the log states the choice once, and counts the dispatches
left without a barrier every 50,000.

**Checks**
- `tests/test_game_order.py`: the rules as written.
- The barrier-related tests pass (`mesh_fallback`, `dump_readback`,
`present_aspect`, `upscale_fallback`, `vulkan_clear_mip`).
- `vk_texture.cpp` compiles. `vk_draw.cpp` needs Boost, which this
machine lacks, so CI compiles it.

**Not yet verified in-game:** GPU frame time A/B on this NVIDIA card
plus the flash check (every-frame shots).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2559
Draw statistics show a combined program key, making suspicious draws
hard to match to native shaders. Cached programs may have no translation
log with those keys.

- Snapshots four keys per accepted query; retains no program pointer and
keeps distinct tuples separate.
- Preserves count rows, GPU commands, cache formats and defaults; adds 1
MiB of query metadata.

- `gpu/vk/vk_draw.cpp`: snapshot stage keys and append counted `code`
rows after successful readback.
- `tests/test_draw_stats_codes.py`: exercise the real producer and
formatter with synthetic inputs.
- `docs/development.md`: explain the trigger and key slots.
- `docs/tested-configurations.md`: record the earlier AMD run.

- `python -I -B tests/test_draw_stats_codes.py --cxx clang++`: identity,
grouping, lifetime and capacity. MinGW, MSVC CRT and GCC pass. Fails on
the old code, passes now.
- `python -I -B tests/run_all.py --jobs 4 --timeout 120`: Windows/Linux
CI passes. Locally: 489 passed, 3 failed, 37 skipped; FPX/spawn tests
lack C++ headers and NUP fixtures lack Windows symlink privilege.
- RX 9070 XT, Hyrule Field, 84 seconds: 2,920 queries and 442 key
groups; totals match. Earlier API19 port, validation off. Tested on AMD
only. No game data is included.

- API21 game integration and full validation untested.
- Linux gameplay, NVIDIA and Intel untested locally.

---------

Co-authored-by: OpenAI Codex <[email protected]>
Co-authored-by: NotRexed <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2896
## Problem

When uploading textures in `staging()`, if the current recording's
timeline value (`rec_value`) has advanced past an existing 64 MB staging
arena's `a.value` while that arena is still in-flight on the GPU
(`a.value > completed_value`), `staging()` previously skipped the arena
entirely and allocated a brand-new 64 MB `VkBuffer` via VMA — even when
the in-flight arena had tens of megabytes of untouched tail space
remaining.

Because OoT submits ~6 times per frame, consecutive submissions that
each upload small textures (e.g. a few KB to a few MB) while prior
submissions are still in flight forced `staging()` to allocate 4–7
separate 64 MB staging arenas (256–448 MB of host-visible/BAR memory)
and stalled the replay thread in `vmaCreateBuffer`.

## Solution

Add a third pass in `staging()` before allocating a new buffer: if an
existing arena is in-flight (`a.value < rec_value`) and its aligned
unused tail (`at = (a.used + align - 1) & ~(align - 1)`) fits the
requested upload (`at + n <= a.size`), suballocate from `[at, at + n)`,
advance `a.used = at + n`, and update `a.value = rec_value` so the arena
is not recycled from offset 0 until all submissions reading any slice of
it have completed.

Because `align` is at least `nonCoherentAtomSize` (and $\ge 256$),
flushing `[at, at + n)` never overlaps cache atoms still being read by
earlier in-flight slices `[0, a.used_prev)`.

## Measurements & Verification

- **In-game A/B measurement (OoT, Linux x86_64, RTX 4050 Laptop GPU 6
GB, NVIDIA 615.78.08, 3,203 frames across Boot $\rightarrow$ Intro
$\rightarrow$ Hyrule Field $\rightarrow$ Market Town $\rightarrow$
Hyrule Castle)**:
  - **Before https://github.com/NotRexed/Hookshot/pull/2469 (`main`)**:
- Consecutive in-flight submissions uploading small textures allocated
**4 to 7 separate 64 MB staging arenas** (`vk: staging arenas: 1`
through `vk: staging arenas: 4`+), logging **7 slow arena creation
stalls** per session on the replay thread (`vk: a staging arena of 64 MB
took 145.1 ms to make`, `112.7 ms`, `12.1 ms`, `8.1 ms`, `6.1 ms`, `6.0
ms`, `5.6 ms`, `5.2 ms`).
  - **With https://github.com/NotRexed/Hookshot/pull/2469 (`3f9d56e2`)**:
- **Boot through Frame 1,420 (142 textures, 137 MB uploaded across
dozens of submissions)**: All uploads suballocated into the single
initial 64 MB arena (`vk: staging arenas: 1 (64 MB in this one)`). Zero
additional arenas were allocated, and the Frame 600 and Frame 1,200
memory reports both stayed at:
      ```
[hos Presentation Thread] vk: memory: working set 4812 MB (peak 4812),
private 914 MB, staging 64 MB in 1 arenas
[hos Presentation Thread] vk: memory: working set 6523 MB (peak 6523),
private 702 MB, staging 64 MB in 1 arenas
      ```
- **Full 3,203-frame route (Hyrule Field $\rightarrow$ Market Town
$\rightarrow$ Castle, >2,500 textures uploaded)**:
- Arena count **never reached 4** (peaked at 3 only during a single
frame that uploaded two 56.5 MB `1365x1365x90` ASTC arrays = 166.5 MB in
one frame, and trimmed back to `staging 64 MB in 1 arenas` at Frame
1,800 and Frame 3,000).
- Slow arena creation stalls (>5 ms) dropped from **7 per run** to **0
across the first 3,029 frames** (and **1 total** in 3,203 frames, when
two 56.5 MB `1365x1365x90` arrays were uploaded back-to-back at Frame
3,029).
- **Test suite**:
- `python3 -I -B tests/test_texture_staging_publication.py --headers
/usr/include` (including `inflight-tail` on `coherent` and
`noncoherent`: PASS)
- `python3 -I -B tests/test_texture_staging_publication.py --cxx clang++
--headers /usr/include --sanitize` (ASan + UBSan: PASS)
- `python3 -I -B tests/test_ui_texture_upload.py --headers /usr/include`
(PASS)
- `python3 -I -B tests/test_texture_squeeze.py --headers /usr/include`
(PASS)

Co-authored-by: erfboom <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2469
## Problem

Mesh shader draws (`ProgramKind::Mesh`) previously returned
`mesh_program_for(s, color_types)` at the top of `program_for()`,
bypassing both the thread-local 2-slot fast pair (`prog_fast`) and the
thread-local `picks` map, and acquiring `shader_mu` (plus running
`codes_of(s)`, `mesh_key()`, and `still_valid()`) on every mesh draw
call.

## Solution

1. **Thread-Local Pick Caching**: Allow `ProgramKind::Mesh` to query and
populate thread-local `prog_fast` and `picks` caches before falling back
to `mesh_program_for()`.
2. **Prog Match Optimization**: In `prog_same()`, skip vertex attribute
comparisons for mesh shaders (`kind == 2`) since mesh stages do not read
vertex attribute arrays.
3. **Stage Input Hashing**: In `program_for()`, compute mesh input
hashes from `color_types` without vertex attributes and include `stages`
and `kind` in the `picks` key so mesh, graphics, and compute picks never
collide.

## Measurements & Verification

- **In-game A/B measurement (OoT, Linux x86_64, RTX 4050 Laptop GPU 6
GB, NVIDIA 615.78.08, native mesh shaders on, ~40,000+
`DrawMeshTasksIndirect` calls across Hyrule Field $\rightarrow$ Market
Town $\rightarrow$ Hyrule Castle)**:
- **Before https://github.com/NotRexed/Hookshot/pull/2476 (`main`)**: Mesh draws returned early before
`++st_calls`, bypassing `prog_fast` and `picks` and acquiring
`shader_mu` on 100% of the 40,000+ mesh draw calls. In Hyrule Field (the
`600000` $\rightarrow$ `800000` pick window), `vk: program picks:`
logged:
    ```
[hos Presentation Thread] vk: program picks: 444750 kept (195360 by the
last picks' textures, a uniform buffer bound since), 1268 new, 153981
with other textures (of 600000)
[hos Presentation Thread] vk: program picks: 580680 kept (224336 by the
last picks' textures, a uniform buffer bound since), 1707 new, 217612
with other textures (of 800000)
    ```
*(Window delta: 135,930 kept of 200,000 [68.0%], +28,976 `by_tex`
fast-pair hits; second baseline run: 136,454 kept [68.2%], +31,760
`by_tex` hits).*
- **With https://github.com/NotRexed/Hookshot/pull/2476 (`cd99cf4f`)**: Mesh draws flow through `program_for()`
and hit `prog_fast` (including across per-draw uniform/SSBO binds via
`by_tex`) and `picks`, only calling `mesh_program_for()` on first-time
misses:
    ```
[hos Presentation Thread] vk: program picks: 448999 kept (205734 by the
last picks' textures, a uniform buffer bound since), 1545 new, 149455
with other textures (of 600000)
[hos Presentation Thread] vk: program picks: 595137 kept (268209 by the
last picks' textures, a uniform buffer bound since), 1817 new, 203045
with other textures (of 800000)
    ```
*(Window delta in Hyrule Field: **146,138 kept of 200,000 [73.1%]** and
**+62,475 `by_tex` fast-pair hits** — **2.0x** more fast-pair hits in
the 200k window, with `with other textures` dropping from 63,631 to
53,590).*
- **Replay `programs` time in heavy scenes (~2,500–3,000 draws/frame)**:
- Before https://github.com/NotRexed/Hookshot/pull/2476: `programs 3.47 ms` (2,891 draws at 1320 presents),
`programs 3.60 ms` (3,094 draws at 2160 presents), `programs 4.71 ms`
(2,963 draws at 2160 presents).
- With https://github.com/NotRexed/Hookshot/pull/2476: `programs 2.77 ms` (2,762 draws at 1560 presents),
`programs 2.93 ms` (2,687 draws at 2520 presents), `programs 2.66 ms`
(2,067 draws at 2880 presents).
- **Test suite**:
- `python3 -I -B tests/test_program_answer_domains.py --headers
/usr/include` (all 7 domain configurations: 0 failures)
  - `python3 -I -B tests/test_mesh_fallback.py` (PASS)
  - `python3 -I -B tests/test_state_list_plain.py` (PASS)
  - `python3 -I -B tests/test_seed_variant_cleanup.py` (PASS)
- `python3 -I -B tests/test_pipeline_state_portable.py --headers
/usr/include` (PASS)

Co-authored-by: erfboom <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2476
## Problem

1. When `/dev/udmabuf` exists on the host system but persistently
rejects guest memory before any import has succeeded (e.g. kernel/driver
rejection with `EINVAL`, `EBADFD`, or `EPERM`, or when the driver
reports no valid memory type, or a non-`VK_ERROR_OUT_OF_DEVICE_MEMORY`
import failure), the Vulkan backend continues attempting udmabuf
creation on every buffer pool allocation instead of falling back to
host-pointer imports (`VK_EXT_external_memory_host`).
2. Pools that failed `create_device_buffer` in `mirror_make` under a
transient VRAM spike remained permanently marked as `MIRROR_REFUSED`
(`state 3`). Even after texture retirement or VMA compaction freed VRAM
and mirror budget headroom was available, those pools were never
retried, forcing the GPU to read them over PCIe for the rest of the
session.

## Solution

- Add `disable_dmabuf_imports(const char* why)`: if udmabuf creation or
driver import is rejected before any successful import has occurred
(excluding transient `VK_ERROR_OUT_OF_DEVICE_MEMORY`), set `dmabuf_memfd
= -1` (leaving `dmabuf_dev` open so background import threads cannot
race on the fd) to fall back to host-pointer imports for subsequent
allocations.
- In `mirror_make`, if `create_device_buffer` fails (for a pool $\le$ 1
GB), set `p->mirror_retry_epoch = mirror_epoch + 120` rather than
refusing mirrors permanently.
- In `gpu_buffer_bind`, once `mirror_epoch >= p->mirror_retry_epoch`,
check if the pool fits within the mirror budget (`mirror_bytes + (p->hi
- p->lo) <= mirror_budget()`). If room exists, transition `p->mstate`
back to `MIRROR_NONE` so its mirror is requested at the next submission
boundary; otherwise, defer `mirror_retry_epoch` by another 120
submissions without evicting active mirrors or churning on low-VRAM
cards.
- Add `mirror_bytes` and `mirror_budget()` stubs to
`tests/direct_buffer_writes_test.cpp`.

## Measurements & Verification

- **A/B in-game verification (OoT, Linux x86_64, RTX 4050 Laptop GPU 6
GB, NVIDIA 615.78.08, 2048x1152, `RX_VK_VRAM_MB=2240`,
`RX_VK_MIRROR_MB=2048`)** — identical route: Hyrule Field $\rightarrow$
Market Town $\rightarrow$ Hyrule Castle (~2,680 frames):
- **Without https://github.com/NotRexed/Hookshot/pull/2482 (`main` at `20d50332`)**: Upon entering Castle during
a VRAM spike (`image over budget`), two pools (`0x7ee5400000` 172 MB and
`0x7fb2c00000` 40 MB) entered `state 3` (`MIRROR_REFUSED`). Even after
`VRAM compaction 1 over: ... 35 blocks (560 MB) given back to the
driver` and mirror usage dropped to 1,652 MB / 2,048 MB, both pools
remained stuck in `state 3` for all 7 remaining 600-submit summaries,
reading **4,850.1 MB total over PCIe** (474.4–1,219.8 MB per 600
submits, e.g. `vk: bound in guest memory, by pool ... 1219.8 MB in all
... 0x7ee5400000 (172 MB, state 3): 6657 binds, 1211 MB; 0x7fb2c00000
(40 MB, state 3): 170 binds, 9 MB;`).
- **With https://github.com/NotRexed/Hookshot/pull/2482 (`46497054`)**: On the same Castle entry spike, two pools
(`0x7eec000000` 56 MB and `0x7fb3400000` 24 MB) entered `state 3` for
**only 1 summary** (`270.1 MB in all: 0x7eec000000 (56 MB, state 3): 611
binds, 269 MB; 0x7fb3400000 (24 MB, state 3): 14 binds, 1 MB;`). Once
textures retired and budget room opened 120 submissions later, both
pools exited `state 3` and got their VRAM mirrors back (`vk: a frame:
mirrors 312 (1800 MB)`). Across the next 5 consecutive 600-submit
summaries (3,000 submissions through Castle), **0 pools were in `state
3` and 0 MB was read over PCIe**.
- Also verified by @felipefl142 across a 2.5 h full-game completion run
on RTX 3070 Laptop 8 GB (CachyOS) and @YakuzaFanA on RTX 5060 Ti 8 GB
(Windows 11).
- **Test suite**: `python3 -I -B tests/test_direct_buffer_writes.py`
(12/12 pass) and `tests/run_all.py`.

---------

Co-authored-by: erfboom <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2482
The game calls a few nn::os and host paths thousands of times a frame,
and each call does work whose answer is already known. Each change below
removes that work and keeps its result. They are small, and together
they are hard to measure apart (see Measured).

## The change
- **The calling thread's own ThreadType (`host/hos_thread.c`
`thread_arg`).** GetThreadPriority, GetThreadId, ChangeThreadPriority
and the others scanned every thread under `threads_lock`. The game asks
about its own thread most of the time
(`GetThreadPriority(GetCurrentThread())`, about 2,400 a frame in Hyrule
Field).
- When the ThreadType is the caller's own, the caller's record answers
without the scan.
  - Any other ThreadType is looked up as before.
  - Same result, so no switch.
- **SignalConditionVariable with no waiter (`host/nn_os_sync.c`).** It
returns at once when the condition variable's waiter count is 0, without
the lock pair and wake call. `HOS_CV_SIGNAL_ALWAYS=1`: as before.
- **Ordering:** a waiter counts itself in (`cv_count_in`, a seq-cst add)
before it takes the condition variable's lock, and so before it gives
the guest mutex back (`mutex_release_all`). A signaller that holds the
mutex therefore sees the waiter, and the waiter cannot be skipped. A
signal that reads 0 was made before the waiter started to wait. The
bookkeeping under the lock (mixed mutexes, hand-off reservations) uses
its own count, `cv_in`, as before.
- A signaller that does not hold the mutex could already miss a waiter
that had not yet started to wait, before this change as well (that is
the condition-variable contract).
- **Switches read once.** These were read from the environment on every
call: `menu_vsync` (`RX_VK_FIFO` / `RX_VK_IMMEDIATE`, twice a present)
and `HOS_FS_OVERLAY` (every OpenFile). They hold for a whole run. Each
is now read once and published atomically (`HOS_FS_OVERLAY` as one
atomic pointer).
- `RX_VK_DUMP_STAGES` stays a per-read `getenv`: `vk_shader.cpp` is part
of the shader-cache stamp, so changing it would make every player's
prepared shaders stale.
- `RX_VK_TAAU_KEYS` stays a per-dispatch read:
`tests/test_taau_env_lookup.py` pins that.

## Measured
Windows 11, Ryzen 7 5700X3D (16 logical processors), RX 9070 XT, AMD
driver 26.10.2. Standard Hyrule Field test: right stick held right from
the load, `HOS_FPS_MAX=240`, 2560x720, 20 s window, warm caches, 6
alternating pairs a cell, base main 46780503 (with https://github.com/NotRexed/Hookshot/pull/2602).

These changes were measured only together with https://github.com/NotRexed/Hookshot/pull/2709 (both over the
base; 8 and 6 threads: the game held to `0xFF` / `0x3F`):

| threads | arm | fps | CPU ms a frame | p99 | worst | runs with a frame
over 100 / 250 ms |
|---|---|---|---|---|---|---|
| 16 | main | 52.85 | 77.3 | 25.7 | 439.1 | 1 / 1 |
| 16 | this PR + https://github.com/NotRexed/Hookshot/pull/2709 | 52.7 | 72.9 (−5.8 %) | 24.7 | 87.9 | 0 / 0 |
| 8 | main | 44.05 | 87.1 | 29.4 | 88.9 | 0 / 0 |
| 8 | this PR + https://github.com/NotRexed/Hookshot/pull/2709 | 44.05 | 80.1 (−8.0 %) | 29.0 | 60.5 | 0 / 0 |
| 6 | main | 41.35 | 80.93 | 33.0 | 136.5 | 1 / 0 |
| 6 | this PR + https://github.com/NotRexed/Hookshot/pull/2709 | 40.7 (−1.6 %) | 80.91 | 32.1 | 117.5 | 1 / 0 |

- The 6-thread cell was measured with https://github.com/NotRexed/Hookshot/pull/2709's 5 µs spin, which https://github.com/NotRexed/Hookshot/pull/2709
then limited to 7+ logical processors, so it does not apply to this PR
alone; no 6-thread cell was run with that limit.
- This PR's own share is not separated: the own-thread lookup and the
switch reads have no switch to compare, and the Signal change was not
measured alone on this base.
- A counter run on 46780503 (standard test, n=1) counted ~2,800
SignalConditionVariable calls a frame in the play window, none of them
with a waiter: the case this early return skips. (During the load,
170-304 per 5 s found a waiter.) This counts the calls; their time is
not measured separately.
- Earlier figures on 57ed3aa8 (before https://github.com/NotRexed/Hookshot/pull/2602's fix of a main regression,
41 → 53 fps on this test) are replaced by these.

## Merge order with https://github.com/NotRexed/Hookshot/pull/2765
Both PRs change `host/nn_os_sync.c` next to Signal and merge without a
textual conflict. Whichever lands second needs a short follow-up:
- https://github.com/NotRexed/Hookshot/pull/2765's Broadcast reserves the mutex for `cv_in - cv_owed`, the
waiters under the condition variable's lock, instead of `cv_waiting`.
- https://github.com/NotRexed/Hookshot/pull/2765's test fixture gains `cv_in`.
- A new test case: a waiter counted in but not yet under the lock must
not be owed the mutex.

The follow-up is ready and will be pushed right after the first of the
two merges.

## Tested
- `tests/test_thread_arg_self.py` (new): `thread_arg` from the source,
over own and other ThreadTypes. Broken variants must fail: every call
scanning, the caller's record returned for any ThreadType.
- `tests/test_cv_signal_early.py` (new): Signal over waiter counts, with
and without `HOS_CV_SIGNAL_ALWAYS`. Broken variants must fail: the early
return taken even with `HOS_CV_SIGNAL_ALWAYS=1`, the early return
reading the wrong count, a waiter that does not count itself in.
Structural checks: in both waits, `cv_count_in` comes before the
condition variable's lock is taken, and `cv_enter` before
`mutex_release_all`.
- `tests/test_cv_signal_race.py` (new): the real Signal, Wait and
TimedWait on threads (pthreads for the Win32 calls). A waiter held
between its generation read and `cv_enter` while a thread that does not
own the mutex signals must wake (20 rounds each wait, with and without
`HOS_CV_SIGNAL_ALWAYS=1`). 20,000 hand-offs signalled under the mutex
must never stall. It fails on the earlier head that counted the waiter
under the lock, and on a broken variant doing the same.
- `tests/test_vsync_row.py`: one process per switch setting now (the
value is read once), with a negative control.
- `tests/run_all.py`; syntax compile of every changed `host/*.c`; a
gpt-6.1-sol review (three rounds, last clean).

## Not tested
- This PR alone in the game; other scenes; Linux.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2708
## Why

The renderer kept render-target images that newer render targets had
already drawn over, because each newer target was judged alone: two or
more partial overlaps that together cover an eighth of an old target
were ignored. In https://github.com/NotRexed/Hookshot/pull/2180's guard room the target images then grew to 3-4
GB, VRAM reached the driver's allowance and the main and presentation
threads stopped. On our A/B save, v2.1.0 let go 4,727 dead targets per
run.

Fixes https://github.com/NotRexed/Hookshot/issues/2180 - v2.1.0, RX 9070 XT, 4K HDR + frame generation: render
targets remade ~3.6 times a frame, VRAM reaches the allowed limit, then
the main and presentation threads stop (freeze on breaking a jar)

Issue: https://github.com/NotRexed/Hookshot/issues/2180

## What changed

- `target_overlap.h`: `DrawnOver` adds up the shares of several newer
render targets that drew over an old one (the candidates come in start
order) instead of judging each one alone against the eighth. A view
inside an old target is still not a new target (5bfa6aa, https://github.com/NotRexed/Hookshot/pull/899). With
views excluded, every candidate over the start clips to `lo` and every
one over the end to `hi`, so each side keeps only its longest: the sum
differs from one over the whole span only when one newer target covers
the head and another the tail (each under an eighth, together an eighth
or more).
- `vk_texture.cpp` `texture_frame`: the dead-target rule uses that sum.
- `vk_texture.cpp` `destroy_later`: a render target's image goes to the
pool as it is evicted, not at the next present. Between the two the next
frame is recorded, and its target of the same create info made a new
image while the old one was destroyed unused later.
- Tests: `tests/test_target_overlap.cpp` (the union and its edge cases),
`tests/test_target_image_reuse.{cpp,py}` (an evicted image is pooled at
once and taken by the next frame, with stubs, plus source checks),
`tests/test_target_image_pool.py` updated for the new eviction path.

## Testing

Game-free: `python -I -B tests/run_all.py` in the worktree, full suite.

- After rebasing on `upstream/main` 4dd972a8: 494 passed, 0 failed, 35
skipped (Windows, llvm-mingw).
- Before the rebase, on 51da0464: 462 passed, 1 failed, 33 skipped; the
failure was `tests/test_input_background.py`, a declared known
environment failure on this PC (no MSVC SDK). The other declared
environment failures passed in both runs.
- Focused: `test_target_image_reuse`, `test_target_image_pool`,
`test_target_overlap` pass.
- CI: Windows Python source checks part 1/2 passes. Its earlier failure
was the fixture's `getenv` deprecation under the MSVC CRT; fixed in
5a408681 with `-D_CRT_SECURE_NO_WARNINGS`, as the other source-check
fixtures do.
- Still to add: the VRAM and `textures retired` MB at start and after a
few minutes, and a run across Kokiri -> Hyrule Field -> Castle Town and
back with screenshots. The A/B above is Kokiri only; the guard room was
not visited in this run.

In game: NVIDIA GeForce RTX 5070 12 GB, driver 32.0.16.2114 (Vulkan
1.4), Windows 11 build 26200, i5-14600KF. Hookshot v2.1.0, Vulkan back
end, windowed 3840x2160, render resolution 100%, frame generation on,
Kokiri Forest clearing save, no synthetic VRAM allowance (real VRAM
~6352 MB used of 11175 MB).

Union rule, 2 runs per build:

| build | run | avg FPS | 1% low | p99 ms | max ms | over 33 ms |
|---|---|---|---|---|---|---|
| control v2.1.0 | ctrl-4k-a | 79.78 | 69.16 | 14.46 | 16.8 | 0 |
| control v2.1.0 | ctrl-4k-b | 79.96 | 69.69 | 14.35 | 16.6 | 0 |
| this PR (union rule) | p2180-4k-a | 79.77 | 68.59 | 14.58 | 24.4 | 0 |
| this PR (union rule) | p2180-4k-b | 78.90 | 68.49 | 14.60 | 48.5 | 2 |
| control mean | | 79.87 | 69.40 | 14.40 | | |
| PR mean | | 79.34 | 68.54 | 14.59 | | |

Pool change added, 2 runs per build:

| build | run | avg FPS | 1% low | p99 ms | max ms | over 33 ms | let go
(N / MB) | images reused / made / destroyed |
|---|---|---|---|---|---|---|---|---|
| control v2.1.0 | ctrl-4k-c | 78.71 | 68.49 | 14.60 | 35.9 | 1 | 5735 /
62249 | 5075 / 1211 / 1019 |
| control v2.1.0 | ctrl-4k-d | 79.65 | 69.20 | 14.45 | 16.1 | 0 | 5167 /
55844 | 4368 / 1277 / 1094 |
| this PR | p134b-4k-a | 79.07 | 68.73 | 14.55 | 23.0 | 0 | 7776 / 84298
| 7207 / 1102 / 908 |
| this PR | p134b-4k-b | 79.24 | 69.44 | 14.40 | 15.8 | 0 | 7262 / 78704
| 6397 / 1575 / 1365 |
| control mean | | 79.18 | 68.85 | 14.53 | | | 5451 / 59047 | 4722 /
1244 / 1057 |
| PR mean | | 79.16 | 69.09 | 14.48 | | | 7519 / 81501 | 6802 / 1339 /
1137 |

Path evidence: dead targets let go 4,727 / 51,467 MB (control,
union-rule session) against 7,807 / 85,078 MB with the union rule
(+65%). With the pool change, images made new 2,441 -> 1,339 (the
control's 1,244), pool reuse 4,722 -> 6,802 (+44%), and the union rule's
gains are kept. Speed is neutral within the run-to-run spread (79.87 ->
79.34 -> 79.16 avg FPS). The issue's 16 GB AMD repro was not reproduced
on this 12 GB card; the state line and pool counters are the evidence.

## Relation to https://github.com/NotRexed/Hookshot/pull/2484

https://github.com/NotRexed/Hookshot/pull/2484 (draft) sweeps the dead-target pile more often and drains the pool
faster. This PR changes which targets are dead at all (the union
accounting) and when an evicted target's image is reused. They touch
different hunks of `texture_frame` and `pool_trim`, and can land in
either order.

## AI assistance

Assisted-by: oc-impl-1 (opencode/deepseek-v4.1-flash), oc-upstream-1
(opencode/deepseek-v4.1-flash-free)

Tracking: https://github.com/NotRexed/Hookshot/pull/993 (where https://github.com/NotRexed/Hookshot/pull/2180 was consolidated).
Restored-from: https://github.com/NotRexed/Hookshot/pull/2766
Turning the camera to the Deku Tree hitched for about 60 ms. Both causes
are in texture uploads, and this PR fixes both. It was split out of
https://github.com/NotRexed/Hookshot/pull/1834, as Joey-Magnus suggested, and builds on its own on main.

## What it does

- **No pool import for one upload** (`vk_texture.cpp
upload_unswizzle_gpu`, `vk_memory.cpp memory_guest_contained`):
- **Before**: when no imported pool held the guest memory under a
texture, the GPU unswizzle imported it for that one upload. That took
22-25 ms on the replay thread each time, since the driver locks the
pages, and the Deku Tree turn did three in one frame.
- **Now**: the GPU path is taken only where an imported pool already
holds the whole range, and the rest goes to the CPU path.
- `RX_VK_UNSWIZZLE_IMPORT=1` imports as before (read with `env_on`, as
`test_env_on.py` wants).
- **The CPU unswizzle threaded from 4 MB** (`upload_inner`), not 16 MB.
The scene-size targets uploaded at that moment (2779 × 1318 RGBA8, 15
MB) took about 20 ms each on one thread; on several threads, a 15 MB
level takes a few ms.

## Testing

- **Builds**: `vk_texture.cpp` and `vk_memory.cpp` compile with clang
using `nx2/build.py`'s renderer flags.
- **Tests**: `tests/run_all.py`, the whole suite, locally (clang
targeting MSVC, the CPM Vulkan headers): 433 passed. The failures are
exactly the ones main at 51da0464 has on this machine, all down to the
local toolchain.
- **In game**: the 60 ms hitch was measured on Windows 11 with an NVIDIA
RTX card (OoT v131072) and gone with both changes, as part of the
combined build. This head on its own hasn't had a separate run yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: FIocker <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2785
The short stall when something first comes into view (https://github.com/NotRexed/Hookshot/pull/3013) is one pool
import on the thread that replays the frame, and nearly all of an
import's time is Windows walking the game's heap page by page for the
driver. With this change the imports in play take under 2 ms in place of
15 to 29 ms.

For https://github.com/NotRexed/Hookshot/pull/3013 (it takes the cost out of each import; it does not change how
many there are or where they run).

## Why

- All of an import's time is in `vkAllocateMemory` for the host pointer
(the `vk: import ... allocate` lines; buffer and bind are 0.0 ms).
- That time goes with the address, not the size. In six runs of one
route, imports in the heap's low 2 GB took a median 15 ms, the same for
4 MB and for 88 MB; imports above took 2 to 6 ms for 8 to 140 MB. With
every heap page touched at the start (`HOS_PREFAULT=1`) the low ones
took longer, 21 to 33 ms, not shorter.
- A build that times `VirtualQuery` at each import's start says where it
goes: in the low heap the call takes a median 13 to 17 ms and reports a
region of 5,838 MB. The heap is one run of committed read-write pages,
and Windows answers the question by walking from the address to the end
of the run of pages with the same state and protection. The lower the
address, the longer the walk, and the more of the heap is populated, the
slower each step.
- The imports that land in play are for the game's small pools: 64 bytes
or so in a page of their own, somewhere in the low heap, first bound
(read or written, in a compute dispatch) when what they belong to comes
into view. `RX_VK_ASYNC_IMPORT=1` leaves them where they are (5 on the
replay thread against 6).

## The change

While the driver is asked, the page after the import's range has
`PAGE_EXECUTE_READWRITE` in place of `PAGE_READWRITE`, so the region
that starts at the range ends with it. After the call the page has its
own protection back (`gpu/vk/import_fence.h`, used around
`vkAllocateMemory` in `import_pool`).

- Only a committed `PAGE_READWRITE` page is changed. A reserved page, or
a read-only one, already ends the run and is left alone; one changed by
someone else while the fence stands keeps what they gave it.
- Not with the CPU write watches (`RX_NVN_WATCH_TEX` and the others
`import_pool` already steps around), which change protections
themselves.
- Execute is the only protection that differs from read-write and still
reads and writes the same. Nothing runs from guest memory, with it or
without: the game's code is the lifted host code. It is on one page for
the length of one driver call.
- `RX_VK_IMPORT_FENCE=0` turns it off. Linux is unchanged (no region
walk there; the class does nothing).

## Tested

- Windows 11, RTX 4070 Ti SUPER 16 GB (driver 621.24), Ryzen 7 9800X3D,
32 GB. Renderer builds of main e9975d6c and of this branch (llvm-mingw,
without DLSS) under one executable (main e0e60920), after a full
`--prepare-shaders`, the game running without a window (presents drawn
offscreen at 1920x1080) at 60 fps with scripted input. One route of
about six minutes, four times in a row (main, branch, main, branch): a
save loaded, the teleport cheat to the Market as a child, then walking
and turning there by script.

  | | main | this branch |
  | --- | --- | --- |
  | imports of 2 ms or more, whole run | 56 and 48 | 12 and 13 |
  | their time in all | 788 and 525 ms | 41 and 55 ms |
  | the longest | 35.7 and 17.1 ms | 9.2 and 7.7 ms |
| of them on the replay thread after the teleport | 10 (eight of 14.5 to
29.3 ms) and 6 (15.5 to 17.1 ms) | none and none |
  | `slow submit` lines after the teleport | 25 and 10 | 4 and 3 |
| of those, `dispatch` (where the small pools are bound) | 5 and 4 |
none and none |
  | frame rate at the end | 60.0 | 60.0 |

What is left on the branch: the first import of a run, whose time is in
the one-off `vkGetMemoryHostPointerPropertiesEXT` (9.0 and 7.3 ms; its
allocation takes 0.2 ms), and ranges of 20 to 90 MB at 2 to 8 ms, during
the loads. The slowest frame of a ten-second window is unchanged where
something else is the slowest: the teleport itself, and two `memory
sync` captures of 40 to 60 ms that both builds have.
- The measurements under Why are from the same PC and route: main's
renderer with `RX_VK_ASYNC_IMPORT=1` and with `HOS_PREFAULT=1` (two runs
each beside runs without), and a build that logs `VirtualQuery`'s time
and region at each import, with and without the page after the range
changed (two runs each). In those the changed page was left in place;
with it `VirtualQuery` from the range's start took 0.01 ms and reported
4 MB, and the allocations took 68 and 77 ms in all against 488 and 763
ms.

- `python -I -B tests/run_all.py`, Linux, with the Vulkan headers: 481
passed, 48 skipped, 1 failed. `nx2/test_texture_init_barrier` fails the
same way on main.
- New: `tests/test_import_fence.py`. On Windows its fixture runs the
fence on real pages: `VirtualQuery` from the range's start reports the
whole run without it and the range alone with it; the page reads and
writes while it stands and has its protection back after; a reserved or
read-only page after the range is left as it is; a watch armed on the
page meanwhile stays. On Linux it checks that the header builds and does
nothing. It also checks that `import_pool` holds the fence over its
`vkAllocateMemory`. It fails on main. The Windows fixture, built with
llvm-mingw, passes on Windows 11 on the PC above.
- `tests/test_import_buffer_binding.py` compiles `import_pool` itself,
so its fixture has a stand-in for the fence and now also checks that a
host-pointer allocation is made behind one, a dma-buf's without, and
none is left standing (37 cases pass).

**Not tested:** AMD and Intel cards on Windows (the comment at
`prefetch_chunks` reports 20 to 75 ms imports on an RX 9060 XT, which
may be the same walk; not measured here); Linux in play; a build with
DLSS; a long session.

Restored-from: https://github.com/NotRexed/Hookshot/pull/3043
Split out of https://github.com/NotRexed/Hookshot/pull/887, as asked there ("The 32 MB image-pool block change is
independent and looks worthwhile: please send it as its own PR").

A pool block goes back to the driver only when every image in it is
gone, so one long-lived texture in a 128 MB block keeps the whole block.
The default goes from 128 MB to 32 MB; `RX_VK_IMAGE_BLOCK_MB` still sets
it, and the render targets' pool follows the same constant.

**Measured** (Quadro P4000 8 GB, NVIDIA 582.16, Windows 11, 6.6-6.9 GB
allowed by the driver; v2.0.2 and v2.0.5 with this change):
- 128 MB blocks: 1.1-2.2 GB "free in the blocks" in Kakariko, Hyrule
Field and Death Mountain Trail, while the process was over what the
driver allows.
- 32 MB blocks: 0.8 GB in the same places (1.3 GB once in Hyrule Field).
About 110 blocks for 3.5 GB of images.

**Not tested:**
- On current `main`: the change is this one constant and its comment,
rebased on `main`; I have not built or played `main` with it. The
measurements above predate the render targets' own pool (v2.0.12), which
removes part of the fragmentation this addressed, so the gain on `main`
may be smaller.
- Cards with more than 8 GB, AMD and Intel. More, smaller blocks mean
more memory objects (the comment in the file notes the driver's ~4096
limit).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.2 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2967
**Why.** https://github.com/NotRexed/Hookshot/pull/2826 (mine) made the 90% rule and the driver's limit let go of
their excess and 64 MB, not an eighth of the texture cache. @DFN-master
found a case it makes much worse (issue https://github.com/NotRexed/Hookshot/issues/2780): Lake Hylia reached from
Kakariko on an RTX 3080 10 GB with a 4 GB texture budget set by hand and
`RX_VK_COMPACT_PCT=90` runs at 12.7 fps with 6.3 uploads a frame on
`e0e60920`, and at 42.4 fps with none with `RX_VK_EVICT_EXCESS=0`.

- Their two lines: in the thrash, `textures 4022 MB (budget 3639 MB)`
and VRAM 8,288 of 9,131 MB; with the switch off, 2,946 MB of textures in
6,639 MB.
- So the count was 383 MB over the 90% line at each pass. Trimmed to the
line and 64 MB, the cache stays at the line and every pass is an
eviction; the old cut to 7/8 took the count far enough under it for the
rule to go quiet, and the scene's textures then fit.
- The case https://github.com/NotRexed/Hookshot/pull/2826 was written for is the other end: a cache sitting a few
tens of MB over the line (Castle Town Market on an 8 GB card, 58-84 MB
over on average), where an eighth of the cache is ~400 MB of textures
still in view.

## What

- `vram_compact::excess_small(total, rule_budget)`: an excess of up to a
sixteenth of the rule's budget, half of what the cut to 7/8 takes.
- `rule_target(total, size_budget, rule_budget)`: a small excess is
trimmed as https://github.com/NotRexed/Hookshot/pull/2826 does (the excess and `kExcessSlack`); a larger one is
cut to 7/8 of the rule's budget, as before https://github.com/NotRexed/Hookshot/pull/2826. The budget's size is
unchanged.
- With issue https://github.com/NotRexed/Hookshot/issues/2780's numbers the pass's target is `3,639 x 7/8 = 3,184
MB`, the same as with `RX_VK_EVICT_EXCESS=0`.
- `RX_VK_EVICT_EXCESS=0` still means the cut at every pass.

## Measured

**I could not reproduce the 12 fps thrash on this PC**, so the fix rests
on the two log lines above and on the rule's arithmetic, and needs a run
on the card that showed it. @DFN-master, if you can build this branch:
the Lake with your settings and no `RX_VK_EVICT_EXCESS`.

What I could measure, on an RTX 3060 Ti 8 GB (driver 32.0.16.2114),
Ryzen 5 5600X, Windows 11, 2560x1440, DLSS preset K, `RX_VK_FRAMEGEN=0`,
`HOS_FPS_MAX=240`; `e0e60920` with this change, one build, launches
alternated:

**1. The Market keeps https://github.com/NotRexed/Hookshot/pull/2826's gain.** Reached from Hyrule Field, default
options, 8 s settle, 60 s of the camera turning; this PR against the cut
at every pass (`RX_VK_EVICT_EXCESS=0`), three launches each.

| | this PR | `RX_VK_EVICT_EXCESS=0` |
|---|---|---|
| textures let go for the budget | 6,652 / 5,749 / 7,146 | 14,921 /
17,370 / 13,638 |
| uploads a frame | 2.6 / 3.5 / 2.7 | 5.1 / 6.8 / 5.2 |
| fps mean | 54.8 / 53.3 / 52.7 | 53.5 / 51.5 / 52.8 |
| frames over 50 ms | 2 / 6 / 1 | 3 / 5 / 4 |

A counter in a test build said which way the rules' passes went with
this PR: 189 / 212 / 210 small (58-84 MB over on average) and 9 / 13 /
13 large (224-280 MB over), the large ones in the seconds after
arriving.

**2. Why a share of the budget and not a fixed size.** The same pair
with 128 MB (twice the slack) as the bound: 22-102 passes a launch were
over it (181-199 MB on average), and 15,320 / 9,342 / 8,704 textures
were let go against 15,778 / 14,544 / 10,594 with the cut at every pass.
That gives back about half of https://github.com/NotRexed/Hookshot/pull/2826's gain, so the bound stayed a
sixteenth of the budget (176 MB for this card's Market, 227 MB for issue
https://github.com/NotRexed/Hookshot/issues/2780's 3,639 MB).

**3. The Lake with issue https://github.com/NotRexed/Hookshot/issues/2780's settings on this card**
(`RX_VK_VRAM_MB=4096`, `RX_VK_COMPACT_PCT=90`, Kakariko for 10 s, the
Lake's teleport, 120 s): no thrash in any arm, and no difference between
them. `main` 70.9 / 70.3 fps, this PR 70.2 / 70.2,
`RX_VK_EVICT_EXCESS=0` 70.6 / 69.4; under 0.6 uploads a frame in all
six. On an 8 GB card the rest of the game's VRAM leaves the Lake's
2.2-2.8 GB of textures under the line.

## If this does not hold on the 10 GB card

Then the safe change is to make https://github.com/NotRexed/Hookshot/pull/2826 opt-in (`RX_VK_EVICT_EXCESS=1`),
which costs the 8 GB cards the table above and nothing else. I will open
that instead if the run says so, or if a maintainer would rather have it
now.

## Tests

- `tests/test_vram_compact.cpp`: `excess_small` and the three-argument
`rule_target` with issue https://github.com/NotRexed/Hookshot/issues/2780's numbers (the cut, equal to the
two-argument rule's), the Market's (the trim, unchanged), the edge (a
sixteenth is small, a byte more is not), both rules at once with a small
and a large excess, and a budget under the slack.
- `python -I -B tests/run_all.py`: 490 passed, 2 failed, 36 skipped
(Windows, llvm-mingw). The two: `nx2/test_nup_paths.py`, which fails on
`main` on this PC too, and `tests/test_dlss_sdk_cache.py`, which could
not start its test program once with 12 checks running at once and
passes on its own and with the checks that read this code (`run_all.py
dlss_sdk_cache vram_compact texture_squeeze vram_pressure vram_budget
preset_texture`: 6 passed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.2 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/3020
Two costs found by profiling a 4-core handheld (Steam Deck, RADV Van
Gogh, 15 W), both generic — no Deck-specific defaults, both behind an
env switch that keeps the old behaviour available.

Making a staging arena while ~2.5 GB of pools and textures are held took
**12–25 ms** — and an 8 MB one could take **116 ms**, so it is the
allocator, not the size. The arena the last burst used goes stale after
`RX_VK_STAGING_IDLE_S` (30 s) and is given back, so the first upload of
the next burst makes another. A camera turn brings new textures in, so
it paid for one every time: **28 slow creations in one session**, each a
stall of the replay thread.

Four are now made at replay start, on an empty device, where they are
cheap (`RX_VK_STAGING_PREWARM`, default 4; 0 restores the old
behaviour).

Measured over a 240 s run, same save and settings: **28 slow creations
before, 1 after.**

This is the same failure the comment above the idle free already
describes on other hardware ("a scene change made several right after
the idle ones were freed"), so it should help anywhere memory pressure
makes a 64 MB host-visible buffer expensive.

`texture_view_of` was **0.99 ms a frame** for ~10,600 lookups, each a
hash-map find, and the same slots come round again and again (14.9
descriptors a draw, 725 draws).

A 1024-entry direct-mapped cache now answers the repeats. It holds the
key, the slot's version, the destroy generation and the texture epoch,
and does the epoch's touch itself — so a slot re-registered or a texture
destroyed misses it at once and takes the map's path. **The map stays
the authority**; the cache only answers a lookup whose every input is
the one it was filled with. `RX_VK_VIEW_FAST=0` restores the old path.

Measured back to back, same session, same scene, 75 % internal
resolution:

| | fps | p50 | p99 | 1 % low |
|---|---|---|---|---|
| without | 24.8 | 40.32 ms | 42.37 ms | 23.6 |
| **with** | **24.9** | 40.16 ms | **41.32 ms** | **24.2** |

The average is within noise; the tail is a real millisecond.

- `RX_VK_PROF_FINE` now splits the texture path (storage-pointer test,
handle, view, fingerprint), and the frame statistics say which binding
moved `bind_gen` — counted: ubo 2159, tex 1794, ssbo 601, program 431,
img 84 a frame. That last one is worth knowing: the game genuinely
rebinds ~7 things per draw, which is why 0.9 of draws rebuild their
descriptor set and why the reuse check cannot be relaxed.
- `scripts/chain.py` no longer fails on a thread line without a name.

Based on current `main` (e0e60920); both commits cherry-picked cleanly.
I have not been able to build this branch locally (my build machine is
on the 127 commits behind base), so CI is the first build — say the word
if you would rather I build it first and reopen.

Co-authored-by: RufflezAU <[email protected]>

Ported to Longshot: the texture-view front cache and the statistics only. The staging prewarm (four 64 MB arenas made
in replay_start, RX_VK_STAGING_PREWARM) is left out: the arenas it made went through texture_frame's keep and idle
rules like any other, so on a card with 6.5 GB or less three of the four were freed at the first frame, and all of them
30 s after they were made. Keeping them needs an exemption of its own and a check; until then it cost 256 MB of
host-visible memory at start for little (review of #209).

Restored-from: https://github.com/NotRexed/Hookshot/pull/2960
Hi, a small translator change that came out of chasing slow compute
passes on my Quadro P4000.

**The problem**
`WriteGlobalU8/S8/U16/S16` go through `write_global_cas_loop_func`: a
compare-exchange loop with `OpBitFieldInsert` on the 32-bit word around
the byte. When many invocations write bytes of the same word, most
attempts fail and retry. Also, on the page-table path the loop was
handed the exact byte address as if it were the word's address.

**The change**
- Where the storage buffers have 8/16-bit views, the byte or half is
stored directly at its address (through the bound buffer, or through the
page table). Neighbouring bytes aren't touched, so no loop is needed.
- A half at an odd address is stored as two bytes (it can straddle a
word and a page).
- The loop fallback now looks up the aligned word.
- `StorageBuffer8BitAccess` / `StorageBuffer16BitAccess` (and their
extensions) are declared where the u8/u16 views are made. They were used
without being declared.
- `RX_VK_NATIVE_NARROW=0` keeps the old loop. It's an `RX_VK_NATIVE*`
switch so the program cache follows it.
- I left the float atomic add code alone, main already has its own.

**Tested**
- Builds on current main: `RX_VK_ONLY=1 nx2/build.py --standalone`,
llvm-mingw, "compiled in 88s, 0 failed". (The first run died once with
"Compiler exited with status 1 without diagnostics" on two unrelated
files and passed on a plain rerun, so I think that was my machine.)
- An offline emit check: small IR programs with 8/16/32-bit global
stores and float adds, with 0, 1 and 2 storage buffers, through the real
`CollectShaderInfoPass` + `EmitSPIRV`, then every id operand checked
against `spirv.core.grammar.json`. 18 modules, no undefined or zero ids,
with the change and on plain main.
- `tests/test_cas_loop_ceiling.py`, `test_global_atomic_add_f32`,
`test_global_table_loop`, `test_lower_atomg_cas` pass.
`tests/global_load_16_ir.cpp` built and run by hand: all pass.
- An earlier version of this (on v2.0.5 and v2.0.14) is what I've been
playing with on the P4000 (NVIDIA 582.16, Windows 11).

**Not tested / things to know**
- This exact commit on main has not been run in the game or on a driver,
and I didn't run `spirv-val` on it.
- `tests/global_load_16` uses a default `Profile`, so it still takes the
loop path and never exercises the direct store. Its README still says
narrow stores use the loop. I can add a case for the direct path if you
want it in this PR.
- AMD and Intel: untested. It only takes the direct path when the device
reports 8/16-bit storage.
- I'm not sure the odd-address half split ever matters in practice (the
console's stores are naturally aligned), it's there to be safe.

Sorry if this overlaps with something already in progress, I looked and
didn't find it. Thank you for your time!

Restored-from: https://github.com/NotRexed/Hookshot/pull/2976
https://github.com/NotRexed/Hookshot/pull/2960 clears view_fast() with handle_hits()
in draw_begin_recording, and three checks that cut that function had no ViewFast,
which broke only their builds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bare #NNNN in Longshot's tree links to Longshot's own number. The restored
text's Hookshot pull requests and issues are written as
https://github.com/NotRexed/Hookshot/pull/NNNN (or /issues/NNNN).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
RX_VK_ASTC_BC1, RX_VK_GAME_ORDER, RX_VK_VIEW_FAST, RX_VK_NATIVE_NARROW,
RX_VK_IMPORT_FENCE, RX_VK_UNSWIZZLE_IMPORT and HOS_CV_SIGNAL_ALWAYS were not in the
switch reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(vk): the occlusion wiring check names StatsQuery's arguments as the draw statistics now pass them
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 / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
5ce3b68a55
https://github.com/NotRexed/Hookshot/pull/2896 gives StatsQuery the program, not its
key, so the draw statistics can name native shader keys. #260's check of the guard's
place after it looked for the old call; the order it checks is the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Member

🤖 A look at the Texture detail row (e4d17e98, Hookshot 2557) as restored here, at head 5ce3b68a. We were about to port it on its own for players who miss it from late Hookshot builds, found it here instead, and are leaving it to this PR. Three things it doesn't carry over:

  1. No translations. gpu/vk/menu_i18n.h has nothing for the row's new texts in any of the 13 tables (pt_BR, es_419, es, fr, de, it, nl, pl, ru, ja, ko, zh_Hans, zh_Hant): "Texture detail", "Full", the row's help and the four lines of hn_texture_detail. "Low" and "Medium" are already translated, so in another language the row shows an English name with half its values translated. test_menu_i18n doesn't catch it, since it checks only that each table's texts are the menu's, not that every menu text has a translation.
  2. Auto (this PC) isn't in the Auto line. Since Longshot's Auto (this PC) preset, low_vram_texture_mips() is also true at opts.preset == kAutoPreset && opts.budget == 1. So on a driver budget of 4.5 GB or less, the row at Auto acts as Medium under Auto (this PC) at Minimum's tier as well as under Minimum. The code does the right thing, but hn_texture_detail's Auto line ("…at the Minimum preset: as Medium.") and the comment above texture_detail_dims in vk_texture.cpp only name Minimum. The row is preset_exempt and no preset sets it, so a player's Full/Medium/Low stays as they chose it under every preset, Auto (this PC) included. Worth saying so in the description, because that's the interaction a reviewer will ask about.
  3. A doc comment moved. The row's tx_texture_detail went in between /// Anisotropic filtering: the game's own, or 2x to 16x … and tx_anisotropy, so that comment now sits above the wrong function.

Checks run on this head (Windows, no game): test_menu_sections and test_preset_exempt (68 rows) pass. test_menu_i18n fails, but on a Debug tools text and the same way on main (#273 fixes it). test_low_vram_texture_mips and test_option_ids weren't run because there's no C++ compiler on this PC.

🤖 A look at the Texture detail row (`e4d17e98`, Hookshot 2557) as restored here, at head `5ce3b68a`. We were about to port it on its own for players who miss it from late Hookshot builds, found it here instead, and are leaving it to this PR. Three things it doesn't carry over: 1. **No translations.** `gpu/vk/menu_i18n.h` has nothing for the row's new texts in any of the 13 tables (pt_BR, es_419, es, fr, de, it, nl, pl, ru, ja, ko, zh_Hans, zh_Hant): "Texture detail", "Full", the row's help and the four lines of `hn_texture_detail`. "Low" and "Medium" are already translated, so in another language the row shows an English name with half its values translated. `test_menu_i18n` doesn't catch it, since it checks only that each table's texts are the menu's, not that every menu text has a translation. 2. **Auto (this PC) isn't in the Auto line.** Since Longshot's Auto (this PC) preset, `low_vram_texture_mips()` is also true at `opts.preset == kAutoPreset && opts.budget == 1`. So on a driver budget of 4.5 GB or less, the row at Auto acts as Medium under Auto (this PC) at Minimum's tier as well as under Minimum. The code does the right thing, but `hn_texture_detail`'s Auto line ("…at the Minimum preset: as Medium.") and the comment above `texture_detail_dims` in `vk_texture.cpp` only name Minimum. The row is `preset_exempt` and no preset sets it, so a player's Full/Medium/Low stays as they chose it under every preset, Auto (this PC) included. Worth saying so in the description, because that's the interaction a reviewer will ask about. 3. **A doc comment moved.** The row's `tx_texture_detail` went in between `/// Anisotropic filtering: the game's own, or 2x to 16x …` and `tx_anisotropy`, so that comment now sits above the wrong function. Checks run on this head (Windows, no game): `test_menu_sections` and `test_preset_exempt` (68 rows) pass. `test_menu_i18n` fails, but on a Debug tools text and the same way on main (#273 fixes it). `test_low_vram_texture_mips` and `test_option_ids` weren't run because there's no C++ compiler on this PC.
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 / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'restore/defaults-after-rc4:TrashPanda-restore/defaults-after-rc4'
git switch 'TrashPanda-restore/defaults-after-rc4'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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!288
No description provided.