WIP: chore: restore 81 Hookshot PRs merged on GitHub after Longshot branched #209

Closed
TrashPanda wants to merge 82 commits from TrashPanda/Longshot:restore/github-after-split into main
Contributor

Why

Longshot branched from Hookshot's main at 4dd972a8 (#2888). Ninety more pull requests were merged on GitHub after that, before NotRexed/Hookshot went down, and none of them are in Longshot. Their fixes, tested-configuration rows and tools exist only in mirrors. This PR brings back the 24 that still apply, each as its own commit under its original author.

What changed

Each commit is the merged Hookshot PR, carried through Longshot's rename (hookshot to longshot, .github/workflows to .forgejo/workflows). Where Longshot had changed the same lines since, the conflict was resolved on Longshot's side, and the commit message says how ("Ported to Longshot: ...").

The first 24 (as when this PR opened):

  • fix(agents): a mistyped date in a claim does not stop the test request replay (Hookshot #2949)
  • fix(setup): a rename blocked by a scanner is tried again, and the message carries Windows' error (#3021). Longshot's place_tool now does the whole-folder move through move_retry. The Python setup half is left out because Longshot removed the Python Builder.
  • fix: the readme's work/ffx-sdk folder name had a form feed in place of /f (#2553, readme part only)
  • feat(tests): run_all.py --changed runs the checks that read what changed (#2887)
  • test: one source cutter for the checks, tests/srccut.py, with 80 checks moved to it (#2914)
  • test(vk): execute the startup pool placement and fallback checks (#2877)
  • ci: execute authored Vulkan fixtures on lavapipe (#2866). It is in .forgejo/workflows/ci.yml, started by hand like the other full checks.
  • feat(agents): pin test requests and results to commits (#2830), ported to Forgejo (pr_info reads git.magicbean.dev and keeps Longshot's writer check)
  • feat(dev): test a change against a base, as a pack of what it changes, scripts/dev_packs.py (#2732)
  • feat(scripts): longshot_bench.py, a repeatable benchmark route through the control bridge (#2714)
  • build(release): two shipped pipeline lists (#3029, #3030), and the RTX 5090 fork list's note (#2703)
  • docs: tested-configurations rows from 11 PRs (#2616, #2715, #2757, #2771, #2778, #2840, #2844, #2889, #2912, #3014, #3028)

57 more, merged on GitHub in its last hours (they were in a mirror's later fetch):

  • Input and menu: Auto run (#2747), the touchpad click as a binding (#2989), Mouse 4/5 on Linux (#2961), a control's second key survives a rebind (#2933), custom ocarina buttons and the gyro with the controller in the background or the menu open (#2944, #2962, #2963), the mouse camera when the mouse is released (#2947), the Texture detail row (#2557), a failed save leaves the options file alone (#2966), bug-report save errors (#2948, #2978)
  • Rendering fixes: camera cuts reset upscaler history (#2927), FSR 3.1 on the chain keeps HDR highlights (#2614), the ultrawide sides during the loading cover (#2939), Intel split-submission uniforms (#3000), failed texture uploads retried (#3003), GPU writes into texture storage found under later writes (#3002), the FSR 4 tail write order (#2952), and juliotec's seven Vulkan robustness fixes (#2691, #2718, #2719, #2741, #2742, #2743, #3031, #3032)
  • Memory and speed: opaque ASTC as BC1 (#2554), barrier-ordered back-to-back passes on NVIDIA (#2559), 32 MB image pool blocks (#2967), the 7/8 trim on a large excess (#3020), staging arenas kept and a texture-view front cache (#2960), staging tail suballocation (#2469), thread-local mesh picks (#2476), udmabuf fallback (#2482), three hot-path host costs (#2708), render targets covered by newer ones let go (#2766), unswizzle without a pool import (#2785), pool import without a heap walk (#3043), narrow global stores (#2976), a failed allocation at the commit limit no longer closes the game (#3017)
  • Pipelines and shaders: one play file a session (#2625), a background-prepared update builds the state list first (#2982), a half-written record cut off (#2983), left-out ML networks are not shader failures (#2987), the noperspective workaround baked offline (#2782)
  • Platform and other: ReplayPin on Linux (#2903), the renderer load error logged (#2875), the Linux sound output reopened (#2970), 5.1 speakers counted right (#3044), the floor web above 60 fps (#2929), the scarecrow song with note remaps (#3018), GPU wait statistics (#2897), native keys in draw statistics (#2896), two make_game fixes (#2953, #2968), a stalled preparation not promised (#2985)

One commit of mine at the end: stand-ins in four checks that cut code these commits changed (ViewFast from #2960, Longshot's compile_all() in #2982's check). Upstream's tip had the ViewFast three failing too.

Left out, with the reason:

Hookshot PR Why it is not here
#2972, commit near Windows' commit limit Already in Longshot as #77
#2935, ship third_party/zbic's sources in the AppImage Longshot fixed the same build with its own file list in scripts/linux/appimage.sh
#3015, the Linux game files page scrolls Longshot removed the Linux Builder window (#181)
#2565, name the DLSS license flag Only touched the Builder window, which Longshot removed (#181)
#2591, an up-to-date game offers Update, not Play The Builder window's Play button; in Longshot the launcher is the app (#181)
#2582, a 30 FPS cap Needs a port. It was written for the old cap; Longshot's #7 made the cap apply in play, which it conflicts with throughout host/nn_vi.c and its check. Left for its author or a follow-up
#2946, Auto takes the screen's own size Longshot's #172 moved the output size to the host, which already rounds to 8
#3038, a function with code below its entry Already in Longshot
#2926, the game no longer checks for updates Longshot's #13 already removed the game's update check; the Builder parts went with #181

Flags and settings

All new switches default to the upstream behavior as merged; each =0 gives the old path back for comparisons.

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 (#2557) Auto Auto 4 to 6 GB cards Cards with room Longshot builds
RX_VK_ASTC_BC1=0 All ASTC as BC3, as before (#2554) Opaque ASTC as BC1 Default All - -
RX_VK_GAME_ORDER The game's barriers order back-to-back passes (#2559) On for NVIDIA Default NVIDIA - AMD, Intel
RX_VK_IMAGE_BLOCK_MB=<n> Image pool block size (#2967) 32 Default Low-VRAM cards - -
RX_VK_VIEW_FAST=0 Turns off the texture-view front cache (#2960) On Default CPU-bound scenes - -
RX_VK_STAGING_PREWARM=<n> Staging arenas made ahead (#2960) Upstream default Default - - -
RX_VK_SPLIT_UBFLUSH=1 Uniform updates written at each part of a split submission (#3000) Off on Intel, on elsewhere Default Intel Arc - -
RX_VK_TEMPORAL_CUT=0 No upscaler history reset at camera cuts (#2927) On Default DLSS, FSR - -
RX_VK_FSR3_HDR=0 FSR 3.1 on the chain clamps HDR colour at paper white, as before (#2614) HDR colour kept Default HDR on Native - -
RX_VK_NATIVE_NARROW=0 8/16-bit global stores through the compare-exchange loop, as before (#2976) Direct stores Default - - -
RX_VK_IMPORT_FENCE=0 Turns off the import fence (#2482) On Default - - -
RX_VK_IMAGE_PREALLOC=<blocks> Image blocks made at start (documented by #2877) 15% of the device-local heap, 2 to 24 blocks Default - - -
RX_VK_TEST_ALLOWED_MB=<MiB> Diagnostic cap on the allowance the eviction policy sees (#2877) Off Diagnostics only - - -
HOS_CV_SIGNAL_ALWAYS=1 Every condition-variable signal takes the slow path, as before (#2708) Off: a signal with no waiter is skipped Default - - -
RX_VK_UNSWIZZLE_IMPORT=1 A single GPU unswizzle imports the pool again, as before (#2785) Off Default - - -
RX_VK_LOWER_SKIP=<key prefix,...> Programs left unlowered, for a bisection; now part of the program stamp (#2782) Off Diagnostics only - - -
RX_VK_IR Also logs each left-out network's program (#2987) Off Diagnostics only - - -
RX_VK_PREDICT_S=<s> dev_packs.py --predict-s: stop building predicted pipelines after N seconds (#2732) Off Testing only - - -
LONGSHOT_NO_PREPARE=1, RX_VK_ONLY, RX_RELEASE, RX_TARGET, LONGSHOT_KEEP_BUILD_FILES=1 Existing build switches that scripts/dev_packs.py (#2732) sets for its builds Unchanged - - - -
HOS_INPUT_BACKGROUND=1 Existing; now also covers the ocarina buttons, and stays out while the Insert menu is open (#2944, #2963) Unchanged - - - -
HOS_FS_OVERLAY=<folder> A test: game data files found there are read in place of the game's own Off Testing only - - -

Measurements

No new numbers: these are Hookshot PRs that were reviewed and merged with their own measurements, which are in each commit's message and in the tested-configurations rows. The two pipeline lists change what is prepared ahead and were measured upstream (#3029, #3030).

Testing

Tested: python -I -B tests/run_all.py on Linux (CachyOS, Python 3.14, Vulkan headers found): 522 passed, 3 failed, 30 skipped. Longshot main @ 0c74253d on the same PC: 489 passed with the same 3 failing (test_display_pointer, test_lift_cache, and test_texture_staging_publication, which #215 fixes), so this branch adds no failures. I re-ran the affected checks after each resolved conflict. Rebased on 2331f46c (#210, README only) after that run.

Not tested: a game build or run of this branch. The game-facing commits were tested in the game on Hookshot before they merged. The Windows setup retry (#3021) has not run on Windows. The lavapipe job (#2866) has not run on Longshot's Forgejo runners.

Needs other testers:

  • Windows: the setup's tool install with an antivirus scan holding the unpacked folder, expecting a retry and then success, or the new message naming the folder (request to open)
  • Forgejo CI: a hand-started CI run, expecting the Vulkan fixtures job to find one lavapipe ICD and pass (request to open)

Screenshots

Nothing on screen

Risk and rollback

Each commit is separate and reverts on its own. The riskiest are the memory and texture changes (#2554, #2557, #2960, #2967, #3020), each with an =0 switch or a row to turn it off, and #2830's test-request pinning, which changes scripts/test_requests.py's commands. Old cards still replay, and reverting that commit restores Longshot's current client.

Reviewer notes

Please merge this one with rebase, not squash, so that each of the 29 authors keeps their commit. If you'd rather review it in parts, I can split it into the first 24 and the later 57, or by area (input and menu, rendering, memory and speed, pipelines, platform).

The Hookshot PR numbers in the commit messages point at github.com/NotRexed/Hookshot, which is gone; mirrors of it still have the history.

Issues

See #

Checklist

For everyone

  • One topic. (A restore of 81 separate upstream PRs: see Reviewer notes.)
  • 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. (Upstream measurements only.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A row in docs/tested-configurations.md for each measured run.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.
## Why Longshot branched from Hookshot's `main` at `4dd972a8` (#2888). Ninety more pull requests were merged on GitHub after that, before `NotRexed/Hookshot` went down, and none of them are in Longshot. Their fixes, tested-configuration rows and tools exist only in mirrors. This PR brings back the 24 that still apply, each as its own commit under its original author. ## What changed Each commit is the merged Hookshot PR, carried through Longshot's rename (`hookshot` to `longshot`, `.github/workflows` to `.forgejo/workflows`). Where Longshot had changed the same lines since, the conflict was resolved on Longshot's side, and the commit message says how ("Ported to Longshot: ..."). **The first 24** (as when this PR opened): - fix(agents): a mistyped date in a claim does not stop the test request replay (Hookshot #2949) - fix(setup): a rename blocked by a scanner is tried again, and the message carries Windows' error (#3021). Longshot's `place_tool` now does the whole-folder move through `move_retry`. The Python setup half is left out because Longshot removed the Python Builder. - fix: the readme's `work/ffx-sdk` folder name had a form feed in place of `/f` (#2553, readme part only) - feat(tests): `run_all.py --changed` runs the checks that read what changed (#2887) - test: one source cutter for the checks, `tests/srccut.py`, with 80 checks moved to it (#2914) - test(vk): execute the startup pool placement and fallback checks (#2877) - ci: execute authored Vulkan fixtures on lavapipe (#2866). It is in `.forgejo/workflows/ci.yml`, started by hand like the other full checks. - feat(agents): pin test requests and results to commits (#2830), ported to Forgejo (`pr_info` reads git.magicbean.dev and keeps Longshot's writer check) - feat(dev): test a change against a base, as a pack of what it changes, `scripts/dev_packs.py` (#2732) - feat(scripts): `longshot_bench.py`, a repeatable benchmark route through the control bridge (#2714) - build(release): two shipped pipeline lists (#3029, #3030), and the RTX 5090 fork list's note (#2703) - docs: tested-configurations rows from 11 PRs (#2616, #2715, #2757, #2771, #2778, #2840, #2844, #2889, #2912, #3014, #3028) **57 more, merged on GitHub in its last hours** (they were in a mirror's later fetch): - Input and menu: Auto run (#2747), the touchpad click as a binding (#2989), Mouse 4/5 on Linux (#2961), a control's second key survives a rebind (#2933), custom ocarina buttons and the gyro with the controller in the background or the menu open (#2944, #2962, #2963), the mouse camera when the mouse is released (#2947), the Texture detail row (#2557), a failed save leaves the options file alone (#2966), bug-report save errors (#2948, #2978) - Rendering fixes: camera cuts reset upscaler history (#2927), FSR 3.1 on the chain keeps HDR highlights (#2614), the ultrawide sides during the loading cover (#2939), Intel split-submission uniforms (#3000), failed texture uploads retried (#3003), GPU writes into texture storage found under later writes (#3002), the FSR 4 tail write order (#2952), and juliotec's seven Vulkan robustness fixes (#2691, #2718, #2719, #2741, #2742, #2743, #3031, #3032) - Memory and speed: opaque ASTC as BC1 (#2554), barrier-ordered back-to-back passes on NVIDIA (#2559), 32 MB image pool blocks (#2967), the 7/8 trim on a large excess (#3020), staging arenas kept and a texture-view front cache (#2960), staging tail suballocation (#2469), thread-local mesh picks (#2476), udmabuf fallback (#2482), three hot-path host costs (#2708), render targets covered by newer ones let go (#2766), unswizzle without a pool import (#2785), pool import without a heap walk (#3043), narrow global stores (#2976), a failed allocation at the commit limit no longer closes the game (#3017) - Pipelines and shaders: one play file a session (#2625), a background-prepared update builds the state list first (#2982), a half-written record cut off (#2983), left-out ML networks are not shader failures (#2987), the noperspective workaround baked offline (#2782) - Platform and other: ReplayPin on Linux (#2903), the renderer load error logged (#2875), the Linux sound output reopened (#2970), 5.1 speakers counted right (#3044), the floor web above 60 fps (#2929), the scarecrow song with note remaps (#3018), GPU wait statistics (#2897), native keys in draw statistics (#2896), two make_game fixes (#2953, #2968), a stalled preparation not promised (#2985) **One commit of mine** at the end: stand-ins in four checks that cut code these commits changed (`ViewFast` from #2960, Longshot's `compile_all()` in #2982's check). Upstream's tip had the `ViewFast` three failing too. Left out, with the reason: | Hookshot PR | Why it is not here | |---|---| | #2972, commit near Windows' commit limit | Already in Longshot as #77 | | #2935, ship third_party/zbic's sources in the AppImage | Longshot fixed the same build with its own file list in `scripts/linux/appimage.sh` | | #3015, the Linux game files page scrolls | Longshot removed the Linux Builder window (#181) | | #2565, name the DLSS license flag | Only touched the Builder window, which Longshot removed (#181) | | #2591, an up-to-date game offers Update, not Play | The Builder window's Play button; in Longshot the launcher is the app (#181) | | #2582, a 30 FPS cap | **Needs a port.** It was written for the old cap; Longshot's #7 made the cap apply in play, which it conflicts with throughout `host/nn_vi.c` and its check. Left for its author or a follow-up | | #2946, Auto takes the screen's own size | Longshot's #172 moved the output size to the host, which already rounds to 8 | | #3038, a function with code below its entry | Already in Longshot | | #2926, the game no longer checks for updates | Longshot's #13 already removed the game's update check; the Builder parts went with #181 | ## Flags and settings All new switches default to the upstream behavior as merged; each `=0` gives the old path back for comparisons. | 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 (#2557) | Auto | Auto | 4 to 6 GB cards | Cards with room | Longshot builds | | `RX_VK_ASTC_BC1=0` | All ASTC as BC3, as before (#2554) | Opaque ASTC as BC1 | Default | All | - | - | | `RX_VK_GAME_ORDER` | The game's barriers order back-to-back passes (#2559) | On for NVIDIA | Default | NVIDIA | - | AMD, Intel | | `RX_VK_IMAGE_BLOCK_MB=<n>` | Image pool block size (#2967) | 32 | Default | Low-VRAM cards | - | - | | `RX_VK_VIEW_FAST=0` | Turns off the texture-view front cache (#2960) | On | Default | CPU-bound scenes | - | - | | `RX_VK_STAGING_PREWARM=<n>` | Staging arenas made ahead (#2960) | Upstream default | Default | - | - | - | | `RX_VK_SPLIT_UBFLUSH=1` | Uniform updates written at each part of a split submission (#3000) | Off on Intel, on elsewhere | Default | Intel Arc | - | - | | `RX_VK_TEMPORAL_CUT=0` | No upscaler history reset at camera cuts (#2927) | On | Default | DLSS, FSR | - | - | | `RX_VK_FSR3_HDR=0` | FSR 3.1 on the chain clamps HDR colour at paper white, as before (#2614) | HDR colour kept | Default | HDR on Native | - | - | | `RX_VK_NATIVE_NARROW=0` | 8/16-bit global stores through the compare-exchange loop, as before (#2976) | Direct stores | Default | - | - | - | | `RX_VK_IMPORT_FENCE=0` | Turns off the import fence (#2482) | On | Default | - | - | - | | `RX_VK_IMAGE_PREALLOC=<blocks>` | Image blocks made at start (documented by #2877) | 15% of the device-local heap, 2 to 24 blocks | Default | - | - | - | | `RX_VK_TEST_ALLOWED_MB=<MiB>` | Diagnostic cap on the allowance the eviction policy sees (#2877) | Off | Diagnostics only | - | - | - | | `HOS_CV_SIGNAL_ALWAYS=1` | Every condition-variable signal takes the slow path, as before (#2708) | Off: a signal with no waiter is skipped | Default | - | - | - | | `RX_VK_UNSWIZZLE_IMPORT=1` | A single GPU unswizzle imports the pool again, as before (#2785) | Off | Default | - | - | - | | `RX_VK_LOWER_SKIP=<key prefix,...>` | Programs left unlowered, for a bisection; now part of the program stamp (#2782) | Off | Diagnostics only | - | - | - | | `RX_VK_IR` | Also logs each left-out network's program (#2987) | Off | Diagnostics only | - | - | - | | `RX_VK_PREDICT_S=<s>` | `dev_packs.py --predict-s`: stop building predicted pipelines after N seconds (#2732) | Off | Testing only | - | - | - | | `LONGSHOT_NO_PREPARE=1`, `RX_VK_ONLY`, `RX_RELEASE`, `RX_TARGET`, `LONGSHOT_KEEP_BUILD_FILES=1` | Existing build switches that `scripts/dev_packs.py` (#2732) sets for its builds | Unchanged | - | - | - | - | | `HOS_INPUT_BACKGROUND=1` | Existing; now also covers the ocarina buttons, and stays out while the Insert menu is open (#2944, #2963) | Unchanged | - | - | - | - | | `HOS_FS_OVERLAY=<folder>` | A test: game data files found there are read in place of the game's own | Off | Testing only | - | - | - | ## Measurements No new numbers: these are Hookshot PRs that were reviewed and merged with their own measurements, which are in each commit's message and in the tested-configurations rows. The two pipeline lists change what is prepared ahead and were measured upstream (#3029, #3030). ## Testing **Tested:** `python -I -B tests/run_all.py` on Linux (CachyOS, Python 3.14, Vulkan headers found): 522 passed, 3 failed, 30 skipped. Longshot `main` @ `0c74253d` on the same PC: 489 passed with the same 3 failing (`test_display_pointer`, `test_lift_cache`, and `test_texture_staging_publication`, which #215 fixes), so this branch adds no failures. I re-ran the affected checks after each resolved conflict. Rebased on `2331f46c` (#210, README only) after that run. **Not tested:** a game build or run of this branch. The game-facing commits were tested in the game on Hookshot before they merged. The Windows setup retry (#3021) has not run on Windows. The lavapipe job (#2866) has not run on Longshot's Forgejo runners. **Needs other testers:** - Windows: the setup's tool install with an antivirus scan holding the unpacked folder, expecting a retry and then success, or the new message naming the folder (request to open) - Forgejo CI: a hand-started CI run, expecting the Vulkan fixtures job to find one lavapipe ICD and pass (request to open) ## Screenshots Nothing on screen ## Risk and rollback Each commit is separate and reverts on its own. The riskiest are the memory and texture changes (#2554, #2557, #2960, #2967, #3020), each with an `=0` switch or a row to turn it off, and #2830's test-request pinning, which changes `scripts/test_requests.py`'s commands. Old cards still replay, and reverting that commit restores Longshot's current client. ## Reviewer notes Please merge this one with **rebase**, not squash, so that each of the 29 authors keeps their commit. If you'd rather review it in parts, I can split it into the first 24 and the later 57, or by area (input and menu, rendering, memory and speed, pipelines, platform). The Hookshot PR numbers in the commit messages point at `github.com/NotRexed/Hookshot`, which is gone; mirrors of it still have the history. ## Issues See # ## Checklist ### For everyone - [ ] One topic. (A restore of 81 separate upstream PRs: see Reviewer notes.) - [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. (Upstream measurements only.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated.
`scripts/test_requests.py`'s `until_time` passes any two digits for the
month, day, hour and minute to `time.strptime`, which raises on a date
like 2026-13-12 or 99:99. `replay()` did not catch it, so one mistyped
claim line in the room (`claim: until 2026-13-12 16:40Z`) stopped every
command that replays the room, for everyone, until that line left the
fetched history.

## Change

`until_time` returns `None` for a date that is not one, so the claim
gets the default length, as a claim without a date already does.

## Testing

- `tests/test_test_requests.py`, new cases: claims with month 13 and
with 99:99 replay, and each claim gets the default length. They fail on
main (`ValueError`) and pass here.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
Adds a row to `docs/tested-configurations.md` for an RX 7900 XTX on
Linux with RADV, on the v2.1.0 release. The page had no 7900 XTX, Ubuntu
24.04, GNOME X11 or Mesa 26.2.6 row yet.

- Kokiri Forest, at the stream by the village: 36.2 fps at about 2,726
draws, GPU time 16.8 ms a frame.
- The same view after about 10 minutes in Hyrule Field and Castle Town:
34.1 fps, the same as before exploring at the same draw count. No fade
on this 24 GB card in that session.
- Inside the Deku Tree, a short walk: 45.4 fps at about 811 draws, GPU
time 11.0 ms.

Stock v2.1.0 settings apart from fullscreen, 2560x1440 output and 100%
render resolution. No `RX_VK_` variables. udmabuf access on.

See #2156 (the GPU time numbers next to the 780M ones) and #993 (the
fresh and after-exploring comparison in full).

Assisted-by: Claude Opus 5.5 (measurements, analysis, drafting)

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

Co-authored-by: jabbaxipil <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
**What this is for:** keep the Linux RTX 4090 results from #2595 in the
tested-configurations table. **What is measured:** twelve rows covering
four builds and three scene/settings pairs, with both repeats'
log-window FPS medians and ranges. **Not measured:** other cards,
Windows or presentation through a real window.

## What changed

- Adds v2.1.0, pre-fix main `ef81aee7`, main `46780503`, and this
morning's main `51da0464`, newest build first. The newest rows also
retain their contemporaneous `46780503` controls rather than treating
the overnight figures as that comparison.
- Folds repeats and unchanged cells as the existing table does. Logged
version strings retain the diagnostic-only instrumentation; 240 fps is
explicitly a cap, not Uncapped.

## How they were measured

Arch Linux, i9-13900K, RTX 4090, NVIDIA 615.78.08; 2048x1152, warmed
caches. Timed camera sweeps in Kokiri Forest and Hyrule Field used
alternating runs in exclusive timing holds; every retained run met the
quiet criteria (other CPU below three cores on average and six at the
peak).

These are medians and ranges of complete 120-present **game-log
windows**, not the trace-derived whole-window means in the [overnight
comparison](https://github.com/NotRexed/Hookshot/issues/2595#issuecomment-6819741343)
and [morning
comparison](https://github.com/NotRexed/Hookshot/issues/2595#issuecomment-6823250809).
Extraction checks retained each quoted source line and checked build,
device, settings, quiet eligibility and fault counts. Existing captures
show the loaded scenes; camera views are not pixel-locked.

Documentation-only: `git diff --check` passes. No dedicated
documentation checker or test reading this table was found, so there was
no matching test to run through `tests/run_all.py --strict --jobs 1`. No
code or fixture changed and no new game run was needed.

## Limits

One card, no window, a timed route—not scanout, physical-input testing,
continuous flicker proof or a long-session memory test. Each row carries
the date its runs ended. The fixed-release reference was one overnight
session and was not rerun beside every newer build: its Kokiri Forest
runs ended before midnight (2026-11-11), its Hyrule Field and DLAA runs
after it (2026-11-12), and those two rows say so.

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

---------

Co-authored-by: GPT-6 Astra <[email protected]>
Co-authored-by: Claude Fable 5.2 <[email protected]>
These runs belong in the tested-configurations table. They are the first
rows for DLSS Frame Generation from NGX on an RTX 40 card under Windows.
They also record two open problems with a build and a driver: #2695's 36
fps at 4x, which persists after #2701, and the `framegen_record` crash
that #2658 fixes.

Six rows, all 2026-11-12 on one machine: RTX 4090 (NVIDIA 621.01, 144 Hz
display), Windows 11 Home, i9-13900K. `main` 51da0464 was built from
source with NGX (SDK 310.9.16). Settings: DLSS DLAA (model K) at
2560x1440, frame generation by Auto, frame pacing on, cap 60, V-Sync
Auto. Kokiri Forest from a save.

- 2x, Reflex off and on: DLSS-G made every frame, median 60.0 fps in
play.
- 4x, in a window and fullscreen: the runtime makes 1 frame, so the
renderer's own made them on mailbox
([#2701](https://github.com/NotRexed/Hookshot/pull/2701)). The game
stayed at 36.0 fps (144 / 4), with 0 frames on time in each
frame-interpolation report. Details in the comment on #2701.
- The two crashes on `main` in `framegen_record` (a multiplier step, and
a fullscreen-to-window switch at 4x), and `main` plus #2658 with no
crash over the same steps. Details in the comment on #2658.

Figures come from `hookshot_log.txt` ("N presents, X fps", the
frame-interpolation and DLSS Frame Gen lines). "In play" means the
120-present windows after the load. The feel (smooth, pacing, latency)
is what the tester saw while playing.

Docs only, no code. #2757 and #2769 also add rows at the top of the same
table, so whichever merges later needs a one-line rebase.

Tracking: #2510.

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
The tested configurations had no RTX 5080 on Windows and none of the
runs behind my numbers on #2306 (native float add on storage buffers).
This adds them, so the next tester can see what is already covered.

Four rows, newest first, all on the same PC (RTX 5080 16 GB, NVIDIA
621.14, Windows 11, Ryzen 9 9900X3D, 64 GB), headless at the game's
default 2048x1152 output, the 60 fps cap with normal pacing, Link
standing 150 s, `vk: GPU time` every 120 presents:

| Where | Native add | `RX_VK_NATIVE_FADD=0` |
|---|---|---|
| Kakariko Village, child | 6.70 ms (6.5-7.5) | 7.00 ms (6.7-8.0) |
| Death Mountain Trail, child | 4.60 ms (4.4-5.2) | 4.80 ms (4.7-5.7) |
| Death Mountain Trail, adult | 5.40 ms (4.5-6.3) | 5.60 ms (4.8-6.6) |

- The child runs are a build of #2306 rebased onto main 87991bc
(`v2.1.0-rc.3-249-g48e2fe4`); the adult runs a build of main c544828
with #2694 (the bridge's `age` command, to switch to the adult) and
#2306's commits (`v2.1.0-rc.3-252-g5692d75`). The build strings are `git
describe` of those commits; the run logs are gone.
- Posted on #2306 at the time:
https://github.com/NotRexed/Hookshot/pull/2306#issuecomment-6817565938
(adult) and the child numbers above it.
- Docs only. `python -I -B tests/run_all.py`: 419 passed; the 5 failures
are main's own on this PC (fpx_sticky, spawn_emit, bake_portability,
input_background, lift_cache).
- Not covered: one run each way, no night, no other GPU.

Before opening I checked the open PRs that edit this file (34) and main:
none has these runs.

See #2306

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

Co-authored-by: breakthetargets <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
#2748 (the Windows parallel lift emit) was played before it went in, but
that run was only described in the PR; `docs/tested-configurations.md`
asks for a row per run, so the next tester can see that this setup is
covered.

One row, newest first: build `v2.1.0-rc.3-256-gf79aa054` (#2748's branch
plus the damaged-texture check change of #2754), Windows 11, RTX 4090 on
NVIDIA 621.01, 5120x1440, upscaler and frame generation off, 60 fps cap,
Kokiri Forest from a save. The figures come from `hookshot_log.txt`: 32
windows of 120 presents in Kokiri, median 56.1 fps (43.4-60.6) at
1,764-3,926 draws, no device lost.

Docs only: no code, no test change.

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
The recorder thread (#1473) has so far been measured only on AMD under
Linux. These rows add NVIDIA under Windows. On this CPU-bound machine it
cuts the replay's time by about a fifth, and heavy scenes run 7 fps
faster.

**Setup**
- Hardware: i9-13900K, RTX 4090 (621.01), Windows 11 Home, fullscreen.
- Build: `main` 9b6072cd, built from source (llvm-mingw, no DLSS).
- Settings: 2560x1440 output, uncapped (`HOS_FPS_MAX=1000`; the
GPU-bound wait was 0 throughout).
- Route: Kokiri Forest from a save, the same 2-minute walk each run,
1,000 to 4,500 draws a frame.
- Runs: one warm-up, then base, `RX_VK_RECORDER=thread`,
`RX_VK_RECORDER=thread`, base.

**Replay time on the Presentation Thread, median of the 120-present
windows with 1,000+ draws (first one after the load left out)**

| | base 1 | recorder 1 | recorder 2 | base 2 |
|---|---|---|---|---|
| submit (ms a frame) | 8.74 | 6.98 | 6.97 | 8.62 |
| draw | 5.64 | 4.44 | 4.39 | 5.52 |
| commands | 2.12 | 1.15 | 1.14 | 2.08 |
| fps | 81.9 | 81.7 | 81.7 | 80.8 |

**Fps by draw count (both runs of each arm pooled)**

| draws | base | recorder |
|---|---|---|
| 1,000-1,500 | 90.1 | 87.2 |
| 1,500-2,000 | 84.2 | 84.0 |
| 2,000-2,500 | 78.6 | 81.8 |
| 2,500-3,000 | 72.5 | 75.0 |
| 3,000-4,500 | 65.3 | 72.1 |

**Reading**
- The gain is in `commands` and `draw`, the parts the recorder takes off
the replay thread.
- Below about 2,000 draws the frame rate does not move, so something
other than the replay sets the frame there. The overall median is
therefore flat (81.7 against 81.2) while heavy scenes gain 7 fps.
- Cost: the replay waited for the recorder about 145 ms in each 10 s
(the longest 0.3 ms), and the recorder spun idle about 5 s in each 10 s.
That is half a core, so it may cost more on CPUs with fewer cores.
- No visual difference. The log shows no recorder refusal, and the same
failure lines as base. The tester found the pacing good with the
recorder, and average without.

An earlier session on `main` 51da0464, same machine and route, gave the
same picture: replay −18%, 70 fps against 62 above 3,000 draws.

Docs only, no code.

Assisted-by: Claude Code

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
No run on an RX 7700 XT was recorded yet, and Linux AMD is the tier with
the fewest measured runs. This adds one row to the tested
configurations.

**What was run:** the v2.2.0-rc.1 release AppImage (commit 4aee2365,
`source b73bb675` in the log), on CachyOS (kernel 7.3.3, COSMIC Wayland)
with an RX 7700 XT 12 GB on Mesa RADV 26.2.6, a Core i7-14700KF and 64
GB. Output 2560x1440 fullscreen, the default 60 fps cap. The numbers
come from `hookshot_log.txt`.

**What was measured:** Kokiri Forest, standing still at the same spot,
about 2,600 draws a frame, with three settings in one session:

| Upscaler | fps | Slowest frame of each 600 |
|---|---|---|
| Off | 55.6-56.3 | 23-24 ms |
| AMD FSR 3.1 at 100% | 55.2-55.8 | 23.6-23.9 ms |
| AMD FSR 3.1 Quality (1707x960) | 60.0 (the cap) | 21.6-24.5 ms |

Lowering the render resolution alone reached the cap, so this card is
GPU-bound at native 1440p. Video memory used was 7.8-8.1 of 10.8 GB. The
run got past the opening cutscene with no device lost. 10-12 programs
failed from the title screen on, with nothing visibly wrong. A SCUF
Gaming Controller (`2e95:0504`) is mapped by SDL as an Xbox pad, and
Layout preset Xbox / PC puts its face buttons right.

**Checks:** `python -I -B tests/run_all.py` gives 456 passed, 2 failed,
71 skipped. The 2 failures happen on `main` too, on this machine only
(missing `zstandard` and Tk), and are not touched by a docs change.

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
**Why.** The page has no RX 5500 XT row. With #2715 (RX 5600 XT 6 GB),
this would be the second RDNA1 card without mesh shaders (#1001) on it.
This one has 8 GB and runs the v2.2.0-rc.1 release candidate with a
player's overclock.

**What.** One row: free play as child Link after the three Spiritual
Stones, measured per map from the game's own log (the 5-second fps
lines, `GPU time`, the `status:` lines). Settings: Handheld profile, FSR
3.1 at 75%, frame generation off. For comparison, the same build with
frame generation Auto at 165 Hz. The full numbers and the logs, with the
user name replaced, are in
https://github.com/NotRexed/Hookshot/issues/1001#issuecomment-6842826850.

These are free-play numbers, not the scripted standing runs of #2715, so
they show what a player sees rather than a fixed spot.

Docs only; no test or script reads this page.

See #1001

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
**Why.** The page has no RX 5000 row yet, and RDNA1 is one of the cards
without mesh shaders (#1001) that runs the compute fallback. These are
the first measured runs on one, made from the same save each time so
they can be compared later.

Two rows:
- **Main `de866017`:** Kokiri Forest and Hyrule Field over four runs,
the first start's preparation times on this card (shader programs 120 s,
predicted pipelines 3,603 s, noted pipelines 2,355 s; later starts 11
s), and VRAM at the driver's allowance.
- **One switch a run:** `RX_VK_MIRROR=0`, `RX_VK_PARALLEL=3`,
`RX_VK_PRECOMPILE=0`.

All runs were scripted through the control bridge: teleport, 15 s to
settle, 60 s standing, from a copied save restored before each run. The
fps are the medians of the log's `N presents` lines.

Docs only; no checks read this page.

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
Players on 8 GB cards have no real-play numbers for `RX_VK_COMPACT=1` on
the released v2.1.0, only emulated-allowance runs on a 12 GB card. This
adds two rows from a real 8 GB laptop.

Part of #993.

**What changed:** two rows at the top of
`docs/tested-configurations.md`, newest first, in the page's format:
v2.1.0 release on an RTX 5050 Laptop 8 GB (Pop!_OS, driver 595.104.02),
with `RX_VK_COMPACT=1` (three sessions) and without it (one ~100 min
session).

**How it was measured:** `hookshot_log.txt` only. VRAM share is the
`status: 600 frames ... VRAM X of Y GB` lines past 95% of the allowance;
fps is the "N presents, X fps" windows. Reproducible from the logs; I
can attach them.

- VRAM past 95% of the allowance: 87 of 494 lines without (18%), 7 of
334 with (2%).
- Windows under 30 fps: 2.7% without, 0.1 to 1.2% with.
- Mean fps: 54.1 without; 47.3, 46.2 and 55.7 with. The sessions had
different content, so the fps is **not** a controlled A/B and the rows
say so. The VRAM and stutter figures are the part that repeated in all
three sessions.

Docs only; no code, no game data, no `hookshot_options.ini` or
`hookshot_env.txt` contents beyond the switch named in the row.

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

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
The tested configurations had no RTX 40/50 Windows run of Kokiri Forest,
and no single-PC comparison of main against the builds before
#2125/#2419 and #2602. This adds seven rows from an RTX 5090 +
i7-13700K.

**What changed:** seven rows at the top of
`docs/tested-configurations.md`:
- main `46780503`, standing at the Kokiri village autosave: 2048x1152
uncapped 65.7 ± 1.7 fps (GPU 7.1 ms, no GPU-bound wait), 5120x2160 57.2
± 0.4, and DLSS + DLSS Frame Generation 2x capped at 165: game 61.9 /
shown 123.9. Each row includes the critical path from `HOS_CHAIN` +
`scripts/chain.py`: of 15.9 ms a frame, the Presentation thread's inline
replay is ~12.0 and sead::MainThread ~2.8.
- main `9776c36d` 61.9 fps against `55b976c0` (the last main before
#2602) 47.6, `49f75dc8` (before #2125/#2419) 61.9 and v2.1.0 61.2,
alternated in rounds, with readback waits per game frame (2.4-2.9 /
8.2-9.4 / 0.1 / 0.1 ms).

All builds were made from source with the Hookshot Builder's toolchain
and v2.1.0's profile and hot list. The same autosave was loaded by a
scripted input, a warm-up run came first, and frames were measured 40-70
s after the first present (`HOS_PRESENT_TRACE`). The earlier summary on
#2602 is
https://github.com/NotRexed/Hookshot/pull/2602#issuecomment-6817467208.
A moving-route measurement (running and panning through Kokiri) follows
as a second commit.

**Tests:** docs only; no check reads this file. Nothing on screen.

See #2602

---------

Co-authored-by: Copilot App <[email protected]>
Co-authored-by: NotRexed <[email protected]>
The Windows setup unpacks each part with tar.exe and then renames the
folder (llvm-mingw-<version>-ucrt-x86_64 to llvm-mingw). Right after
tar.exe writes hundreds of .exe and .dll files, an antivirus scan or the
search indexer can still hold one of them open, and MoveFileW on the
folder then fails with access denied (5) or a sharing violation (32).
The setup tried once and said only "Could not set up part 8 of 8
(renaming llvm-mingw-...)", with no Windows error, which matches #3011
(every run, also as administrator).

- `move_retry` in setup.c tries those two errors again every 250 ms for
up to 20 s; any other error returns at once, and GetLastError is the
last try's. It is used for the part's rename and for both moves of the
staged install into place in `publish_install` (not the backup moves).
- The rename's message now carries Windows' error, and for 5 or 32 says
another program is holding the unpacked files, often an antivirus scan,
and names the folder to allow. "Could not finish installing" carries the
error too.
- The Python setup (`setup_parts.py`) does the same on Windows with
`replace_retry` at both `os.replace` calls of `install()`; on other
systems a PermissionError is raised at once as before. Its messages
carry the error text.
- `test_portable_setup_transaction.py` cuts `move_retry` out with
`publish_install` and defines Sleep away, so its denied-move case does
not wait 20 s.

I cannot say which program holds the folder on the reporter's PC; if the
retry is not enough, the next report will carry the error number.

For #3011

## Testing

- `tests/test_setup_rename_retry.py` (new): cuts `move_retry` out of
setup.c and runs it with its own MoveFileW and Sleep, on Linux with gcc:
free, denied 3 times then free (4 calls, 3 waits), sharing violation
twice then free, path not found (1 call, no wait, error 3 kept), denied
for ever (80 calls, 79 waits, error 5). Fails on main (no `move_retry`).
- `tests/test_setup_parts_rename_retry.py` (new): `replace_retry` with
os.replace and time.sleep mocked: two PermissionErrors then free,
FileNotFoundError at once, PermissionError for ever (80 tries), not
Windows at once, and `install()` uses it at both places. Fails on main.
- `python -I -B tests/run_all.py` on Linux: 445 passed, 85 skipped, 1
failed (`test_bake_portability.py`: zstandard is not installed here).
- Not run: the Windows-only `test_portable_setup_transaction.py` and
`test_setup_runtime_paths.py` (CI runs them), and a real setup on a PC
whose scanner holds the folder.

Ported to Longshot: Longshot has no Python setup any more (#171), so setup_parts.py and its test are left out. The tool is
now moved into place by place_tool (Longshot), so move_retry is its whole-folder move, the scanner message is the
tool's, and test_setup_tool_merge.py cuts move_retry out with place_tool.

Co-authored-by: Cursor <[email protected]>
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 <>
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 <>
**What this is for:** execute the real pool constructors behind
[hckrman101's merged
#2711](https://github.com/NotRexed/Hookshot/pull/2711), rather than
checking their wiring only as source text. **What is measured:**
GCC/Clang behavior checks, Windows-target compilation, and execution of
the test on Ubuntu and Windows in the fork. **Not measured:** new
gameplay, AMD or physical 8-GB-card behavior; no renderer behavior
changes here.

Refs #2711

## New coverage
A refused first allocation must retry with zero initial blocks, report
zero kept bytes, and return null if the retry fails. The fixture
compiles the production constructors against a refusing allocator and
also checks default placement, compaction off, explicit/zero counts and
cached reuse. Every original numeric/wiring assertion stays; `--source`
enables the negative comparison. The merged test checked the fallback
assignments as strings but did not execute the refusal.

## Checks
Arch Linux 7.2.15-1-omarchy, GCC 16.2.1 and Clang 23.1.3. Rebased head
`8bf7817f1` on main `4cf785a0b`; the test commit is identical to the
fork-tested version (`605cf9ec7 = 4788c8a77` in the range-diff). Only
independent documentation rows conflicted; both sets are retained, with
our rows together.

| Production source | GCC | Clang |
|---|---|---|
| Merged fix `1c0cd9803` | Six constructor cases and original checks
pass | Same |
| Its parent `b157c3e8` | Four constructor cases fail; compaction-off
and preallocation-off pass | Same |
| Rebased head `8bf7817f1` | Six constructor cases and original checks
pass again | Same |

The negative checks compile and fail behavior assertions, not
missing-string checks.
[Fork run
43268735876](https://github.com/mooki000/Hookshot/actions/runs/43268735876)
on `185a460b`: `tests/test_blocks_at_start.py` passes on Ubuntu (0.8 s)
and Windows (13.8 s).
The run's other failures were `test_readme_menu_rows.py` and, on
Windows, `test_wb_shadow_remake.py` (main failures tracked by #2857 and
#2868), plus `test_release_scripts.py` (the nightly race fixed since by
#2822). The whole workflow was not green; none of those failures is this
test.
Local Windows-target compilation passed both C++ units with `-std=c++20
-Wall -Wextra -Werror`. Earlier full suite: **476 passed, 0 failed, 20
skipped**; not repeated for this documentation-only rebase. The plain PR
gate passes.

## Historical rows, not new timing evidence
Four pre-merge Market runs retain their exact commits (`51da0464`, fix
`234f72a3`, combined `3972579c`) in the tested-configurations table:
Arch/RTX 4090, NVIDIA 615.78.08, 2560×1440, default DLSS model K at
DLAA/100%, ordinary shared-card runs. They are not relabelled as this
head's gameplay evidence.
Under the 7000-MiB/9-block model, utility blocks fell from nine to one
for the same 102 MiB of images; final budget snapshots were 835 versus
1771 MiB. No fatal/device/driver fault; no speed or real-card paging
claim. No new game run was made for this test follow-up.

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

---------

Co-authored-by: GPT-6 Astra <[email protected]>
A full `tests/run_all.py` is about 490 checks and several minutes, and
the ones an edit touches had to be picked by hand, which is how a check
that cuts a function far from the edit gets missed (AGENTS.md: "Tests
read the source"). This is the dev-loop proposal's D1 (#2636).

## Change

`tests/run_all.py --changed [BASE]` takes the files changed since BASE's
merge base (`origin/main` by default), committed or not, plus the
untracked ones, and runs:
- the checks whose text names one of them: by path, by file name
standing alone, or as a module they import (`import chunks`, `from
chunks import`);
- the changed checks themselves, and the byte-compile check;
- the C++ unit tests when something under `gpu/` changed;
- **every check** when the runner itself or a shared file under `tests/`
changed.

It prints how many files changed and how many checks read them, and that
a pull request still needs the whole run. Nothing else in the runner
changes.

## Example

On this branch (the runner changed): `--changed: 2 files changed since
origin/main, every check (the runner or a shared test file changed)`.
For a change to `nx2/chunks.py` alone it picks the chunk, emit and
lift-cache checks (under a quarter of all), not the renderer's.

## Testing

- **`tests/test_run_all_changed.py` (new):** which checks a changed file
picks (by path, by name and not inside a longer name, by import), that a
changed check runs itself, that the runner or a shared test file runs
everything, that an unnamed file runs none, and the files from git
(committed, uncommitted and untracked) in a scratch repository.
- **`run_all.py --changed origin/main` on this branch, Windows:** every
check, as above: 423 passed, 21 failed, 79 skipped. The 21 fail the same
way on `main` here (this PC's compilers and Linux-only checks).
- **The checks that read the changed files (10):** pass, apart from
`test_spawn_emit`, which fails the same way on `main` here.

See #2636 (D1).

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

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Follow-up to #1866's review.

- **readme:** the default `HOOKSHOT_FFX_SDK` folder read
`work<0x0C>fx-sdk` (a `\f` had turned into a form feed). It reads
`work/ffx-sdk` now.
- **Windows Builder:** an AMD FSR 4 tick saved before the builder asked
for AMD's license still builds with FSR 4 and fetches nothing, as
before. The build log now says so in one line: "AMD's license not
accepted yet: tick AMD FSR 4 again to have its runtime fetched".
`tests/test_ffx_sdk.py`'s source check covers the line.

Ran locally on Windows: `test_ffx_sdk`, `test_make_game_ngx`,
`test_builder_dlss_update` and `dotnet build` of the Builder, all
passing.

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>

Ported to Longshot: only the readme fix. Longshot's builder is command line only (#181), so the Builder window's
log line and its source check are left out.

Co-authored-by: Cursor <[email protected]>
Follow-up to #2143, which merged before I could answer Joey-Magnus's and
hckrman101's question about `brett-d-rtx-5090-fork-fg-2x`. This changes
only that list's note, to say what its build was.

The "fork" was local builds of main at 0828863 (9 Nov), plus changes
that don't touch what goes into a record: DLSS-NR and an FXAA option in
the window-scaling pass, a sampler cache and its descriptor-reuse check,
a texture budget, menu rows and a voice feature. There are no changes to
shader translation, program variants, target formats or draw state.

`python -I nx2/pipeline_states.py check` passes. The list itself is
unchanged.

Refs #2034, #2143

Co-authored-by: Copilot App <[email protected]>
Co-authored-by: NotRexed <[email protected]>
**Why.** Most performance PRs say "not tested in the game yet", and the
reports that do test can't be compared, because each tester stands
somewhere else for a different time. #2595 asks for a repeatable route
through the control bridge. This is one, run on real hardware: last
night it measured six open PRs and five switches on an RX 5600 XT.

## What it does

`scripts/hookshot_bench.py GAME_DIR --name NAME [--save DIR] [--env
NAME=VALUE]...`

1. Starts the game folder with the bridge on (`hookshot_ctl.py launch`),
with the switches given.
2. Waits for frame 1 in 50-minute steps, up to 8 h, because the bridge
answers a `wait` within the hour and a first start's preparation can
take longer: 1.6 h of pipelines on the RX 5600 XT.
3. Presses A until the newest save is loaded.
4. For each place (default `KokiriForest_Child`, `HyruleField_Child`):
teleports there, waits until no event or load plays, settles 15 s, then
measures 60 s standing.
5. Takes a screenshot of each place, quits, and writes `summary.json`.
For each place it holds the medians of the log's `N presents` fps and
draws, `GPU time`, every part of `replay time (ms)`, the highest VRAM
used, the bridge's fps, the process's memory, and the share of the
window an event played in.

## Two things found the hard way, handled

- **The game saves by itself as it plays.** A slot is rewritten in each
run, and Continue loads the newest, so the second run started somewhere
else. `--save DIR` (with `--slot`) starts every run from the same save:
- **Windows:** the folder's `user/save` is set aside and put back after
the game has closed.
  - **Linux:** `launch --save` copies it into the run's own data folder.
- **A message can wait for a button.** The wake-up cucco hatches by the
game's clock, and its message held a run in an endless "event". An event
that lasts 8 s is closed with A, before the window and in it, and
`summary.json` counts the presses.

## Tests

- `tests/test_hookshot_bench.py` (16 checks, no game, no socket) runs
against a stand-in bridge on a fake clock:
  - the log sums from lines in the game's own format;
  - the first-frame wait going on after timeouts and giving up;
  - A until a save loads;
  - a refused teleport waited out;
  - no map wait when already there;
  - messages closed with A in both places;
- `saves_in` setting aside and putting back, and refusing a folder with
no slots or one left from an earlier run;
  - process memory and liveness on Windows and Linux.
- `python -I -B tests/run_all.py` on Windows: the new check passes; the
same 14 checks fail with and without this change (audio, menu i18n,
docker packages, ... on this Windows box), none in files it touches.
- In the game: Windows 11, RX 5600 XT, main `de866017`, the script
itself with `--save`. Kokiri Forest 33.2 fps, Hyrule Field 35.1, which
matches the local version that ran 23 times last night. The saves were
back in place afterwards.

`docs/control.md` gets a "A benchmark route" section. Its Known limits
now say the bridge has been driven inside a Windows game build
(`launch`, `status`, `wait`, `press`, `teleport`, `shot`, `quit`), which
it had not been before.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
Testing a change in the game means a new full build: a new work folder
translates the game again, and a new game folder prepares every shader
from cold, 69 minutes in all on an RX 9070 XT. Most of that is the same
for every change. This adds a development tool that builds the game once
as a base and each change as a small pack over it.

## What changed

- **`scripts/dev_packs.py`** (new, development only):
- `base` builds a checkout at a commit. `--seed` copies an older game
folder's shader caches first, as an update keeps them.
- `pack` builds a change. Commits, uncommitted edits and untracked files
all count.
- **Renderer alone** (`RX_VK_ONLY`, seconds): when every changed file is
a renderer file the game isn't built from. Those files are found in the
checkout from `host/`'s includes and `build.py`'s dependencies.
    - **Otherwise `make_game.py`**, which builds both modules.
  - **What a pack keeps:**
- **A module** when an object in its link list (`objs.rsp`,
`vk_objs.rsp`) differs from the base's. Bytes can't decide this:
`nx2/version.py` puts a fingerprint of every source into both modules.
- **Both modules** when a build script, a non-translator data file under
`nx2/`, or a non-C file changed.
- **Generated files that differ**, such as the shader seed and the
prompts. Files the base's own build made that the change no longer makes
are removed.
- `rt` makes a runtime: the base with `romfs` hard-linked and the other
files copied, the packs' files over them. It runs a prepare when the
shader translator or the seed changed. Two packs that bring the same
file are refused, and so is a pack built on another base.
- **`docs/development.md`:** a new section, *Testing a change against a
base*.
- **`AGENTS.md`:** one bullet pointing agents to it.
- **Nothing players build changes.** It calls `make_game.py` and
`build.py` as they are; no build or CI file is touched.

## Testing

- **`tests/test_dev_packs.py`** (no game):
  - the build route, and the renderer files the game reads;
- that the translator list matches `build.py`'s stamp, and that the
version-stamped objects are `build.py`'s;
  - that link-only inputs keep both modules;
  - the lift key's dependence on the folders;
- generated files that change or are removed, and a clean output folder;
  - a runtime from packs, and every refusal by its own reason.
- **Negative controls:** every rule above has a sabotaged variant of the
script that fails the test.
- **Ten review rounds** with gpt-6.1-sol, every finding fixed; the last
found none.
- **End to end on Windows** (Ryzen 7 5700X3D, RX 9070 XT), packs over a
full build of 57ed3aa8:

  | Pack | Changed | Kept | Time |
  |---|---|---|---|
| host only | `host/nvn_native.c` | `hookshot.exe` | 14 min first (one
translation), 48 s later |
  | host + renderer | `host/nvdrv.c`, `vk_memory.cpp` | both | 34 s |
| renderer only | `vk_replay.cpp` | `hookshot_vk.dll` | 16 s first, 2 s
later |
| runtime of two packs | | exe from one, DLL from the other | 3 min 51 s
|

Each pack's modules are byte-identical to that change's own earlier full
build, apart from 2 PE header bytes. The renderer-only route gives the
same DLL as `make_game.py` for the same tree.
- **Seeded base** on main 46780503 from 57ed3aa8's caches:
  - 52.6 min, against 69 min for 57ed3aa8 cold.
- **Shader programs:** 290 s against 822 s. main changed the translator,
so the program cache was rebuilt, but the driver's pipeline cache
carried over.
  - **Precompile:** 258 s against 602 s.
  - **Predicted pipelines:** 1,790 s against 2,150 s.
- **In the game:** a runtime made by `rt` from two packs over 57ed3aa8
(the host-only pack's exe, with its new switches off, and the
renderer-only pack's DLL):
- **The run:** the standard Hyrule scene (32:9, unlocked frame rate,
camera turning), 3 runs, RX 9070 XT, Windows 11.
- **Result:** the game started and played the scene every time, at
40.4–41.9 fps. 57ed3aa8's own runtime measured 39.8 fps on the same test
that morning.
- **Not a measurement:** n=1 after two warm-ups, and the game clock
dipped to 0.94–0.98 in these runs, as it did in the base's own runs that
day.
- **No tested-configurations row:** this was a development runtime with
unmerged changes, not a player build.
- **Not tested:** Linux. The module names there are `hookshot` and
`hookshot_vk.so`, and `RX_TARGET=windows` uses the Windows names;
neither `pack` nor `rt` has run there.

Related: the dev-loop proposal (#2636). This covers part of its "middle
loop" without changing the build.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
**What this is for:** testers and authors should be able to tell which
commit a test request asks for, and whether a result covers the current
pull request. This extends the queue from #2734 and #2746 without
replacing its room-message format. **What is measured:** offline queue
tests on Linux, including the old client reading new messages: **225
checks passed**. The complete game-free rerun had **474 passed, 1
failed, 20 skipped**; the failure is the unchanged release-script
concurrency case described below. Three central tests fail against main
for the missing commit tracking. The project's whole check set, both
`windows-2022` parts included, passed on [a run on my
fork](https://github.com/mooki000/Hookshot/actions/runs/43245937287)
before this was opened. **Not measured:** a live claim and result (the
trial card waits for a maintainer's label), or how the Discord relay
treats the added lines beyond what the trial below shows. No game or GPU
is needed for this change.

Refs #2797.

## Problem

A request names a PR, but a PR can change while someone is building it.

On November 12, [#2731's defect
report](https://github.com/NotRexed/Hookshot/pull/2731#issuecomment-6825358246)
named `bce3d17e`. The author [fixed it in
`309e6c35`](https://github.com/NotRexed/Hookshot/pull/2731#issuecomment-6825472223)
at 13:52 UTC. A [test result posted at 14:43
UTC](https://github.com/NotRexed/Hookshot/pull/2731#issuecomment-6826733928)
correctly named its build of `bce3d17e`, but the queue had no way to
distinguish that result from a test of the new head. The test remains
useful evidence for its build; it is not evidence for the later fix.

In `scripts/test_requests.py`, `cmd_request` recorded the PR number, and
`replay` recorded each result and comment link, but neither stored a
commit. The existing GitHub approval response already contains the head
SHA.

## What changed

- **Requests pin a commit.** `request` reuses its one GitHub response to
store the head. `request --commit <sha>` deliberately chooses another
commit of that PR. Values must be 7–40 hexadecimal characters. A head
abbreviation expands to the full head; another value is kept and called
out as an explicit non-head choice. The script checks the spelling, not
membership in the PR or abbreviation uniqueness. If GitHub cannot be
reached, an explicit full 40-character SHA still posts, with approval
and head unknown; without it, the error explains that remedy.
- **Claims say what to build.** The existing approval check also checks
the head. If it moved since the request, the command and rendered card
show both heads and ask the requester to confirm. The pin is not
silently changed, and the request is not closed.
- **Results name the tested commit.** `done --commit <sha>` records the
actual build. Without it, the default is the commit recorded for that
tester at claim time, even if the requester updated the card during the
test. A result differing from the head observed at reporting time says
**“tested an earlier commit”**. GitHub being unreachable or rate-limited
never blocks recording a finished test: it writes the commit without a
head and explains that the comparison was unavailable.
- **An author can update one card.** `update T-XXXX --commit <sha>` is
restricted to the requester and an open card. It keeps the ID, claims,
results and room history. Older results stay visible with their commits,
but do not satisfy the new pin or block another claim. Existing testers
retain the commit they started with.
- **Listing keeps the approval display.** Plain `list` calls the same
annotation path as main, once per distinct open PR. That same answer
supplies the head at no extra API cost. Waiting requests still say they
need a maintainer and appear last. `show` checks its one PR, and `claim`
always checks approval again.
- **The guide and both request/testing instructions** say to build the
card's commit and name the actual build in `done`. The comment still
starts `Test request T-XXXX: pass|fail|mixed`, followed by hardware and
the commit built.

No new package, shell command or platform-specific code is used by the
client. `GITHUB_TOKEN`, then `GH_TOKEN`, remain optional. The script
does not retrieve a token from `gh`.

### Message compatibility

The original first line stays unchanged. `Commit:` and `Head:` lines are
appended **after all existing lines**, including request notes. For
example:

```text
🧪 T-7KQ2 done pass: https://github.com/NotRexed/Hookshot/pull/2731#issuecomment-123
Commit: bce3d17e
Head: 309e6c35
```

Old requests without these fields render exactly as before. The old
client treats added request lines as notes and ignores added
claim/result lines; its result URL is unchanged. The new client's
per-tester commit map is separate from the old numeric claim-expiry map.
An old client's result without a commit still appears, but is not
credited to a known commit. Old clients ignore `update` and cannot
enforce pins.

**The Discord relay is separate software.** This repository does not
build its embeds, and the relay's unknown-field handling is not
documented. Loading the old client in tests proves client compatibility,
not server compatibility. hckrman101, as the relay owner, should confirm
that appended lines preserve the card, and that its commit display and
update handling agree with the client. The guide keeps the old Discord
layout and adds a separate CLI example; it does not claim the live relay
already shows the new fields.

### GitHub calls

For successful commands on open cards, with `N` distinct PRs in a
listing:

| Command | Before | After |
|---|---:|---:|
| `request` | 1 | 1, reused for head and approval |
| `list` | N | N, reused for head and approval |
| `show` | 1 | 1 |
| `claim` | 1 | 1 |
| `done` | 0 | 1 best-effort attempt |
| `renew`, `release`, `cancel` | 0 | 0 |
| `update` | unavailable | 1 |

The existing rate-limit warning remains; there are no retries. Reads
still show the room record when GitHub is unavailable. `done` still
posts with `Commit:` but no `Head:` and a note. `request` can also post
without a successful lookup when `--commit` is a full 40-character SHA;
otherwise it explains that a known full commit or a later retry is
required. Such a card's approval stays unknown until the next successful
lookup. Claiming still requires approval; the new update command
requires a head check.

## How to try it

After the PR is pushed and approved for testing:

```sh
python -I scripts/test_requests.py request --pr <number> --summary "<what to test>" --needs "<hardware>"
python -I scripts/test_requests.py claim T-XXXX
```

Build the commit printed by `claim`, not whichever head a later fetch
returns. Report the actual build:

```sh
python -I scripts/test_requests.py done T-XXXX --commit <built-sha> --result pass --comment <PR-comment-URL>
```

The requester can move the card after a fix:

```sh
python -I scripts/test_requests.py update T-XXXX --commit <new-sha>
```

Use `show T-XXXX` for a fresh single-card view or `list` to refresh the
queue's approval, PR state and heads together.

## How it was tested

Arch Linux, kernel `7.2.15-1-omarchy`, Intel Core i9-13900K, Python
3.12, November 12. Base `51da0464`; head `d22aec74`. No game, GPU or
window. The whole-suite receipt records load average 7.58 → 46.27; this
was a correctness run, not a performance measurement. Tests used fake
room/session/GitHub responses, and command tests reject unexpected HTTP
calls.

### Focused tests and compatibility

`python -I -B tests/test_test_requests.py --old-source
<main-client-copy>`:

```text
pass test_request_head
pass test_moved_head
pass test_done_commit
pass test_explicit_commit
pass test_update_commit
pass test_old_card
pass test_list_calls
pass test_done_unreachable
pass test_request_unreachable
pass test_github_head
LIST CALLS: main=1 branch=1 (two cards, one PR)
pass test_old_client
225 checks
all passed
```

The old-client copy is the actual `scripts/test_requests.py` from main
`51da0464`, loaded alongside the new client. It accepts the new request,
claim and done, retains the exact result URL, treats the request's extra
fields as notes, and ignores the unknown update action. The stored
old-format card test compares its complete rendering byte-for-byte.

The remaining cases cover approval and merged-PR refusals, claim
renewal/expiry, explicit and default commits, malformed hex/lengths,
upper-case and abbreviated commits, a requester update during an active
test, old results without commit metadata, no extra request API call,
equal list-call counts against the actual main client, optional-token
precedence, and GitHub outages/rate limits. Both claimed/default and
explicit done commits still post without Head; requests without a known
full SHA fail with the remedy, while an explicit full SHA posts and
remains approval-unknown until a later lookup succeeds.

### The three central tests against main

The same test file with `--source <main-client-copy> --test request_head
--test moved_head --test done_commit` exits 1, with ordinary assertion
failures rather than a missing option or import failure:

```text
FAIL: request records the head commit
FAIL test_request_head
FAIL: claim tells the tester the pinned commit, not the new head
FAIL: claim reports both heads and asks the requester to confirm
FAIL test_moved_head
FAIL: done defaults to the commit recorded at claim time
FAIL: done records the current head and calls the result an earlier commit
FAIL test_done_commit
21 checks
15 failure(s)
```

All three pass in the branch run above.

### Whole game-free suite

`python -I -B tests/run_all.py --jobs 8`, through the local
idle/core-dump wrappers, with the project's pinned dependencies:

```text
474 passed, 1 failed, 20 skipped in 185 s (linux, 8 at once)
```

Exit 1. The queue tests and tracked-Python source compilation check
passed. The one failure is
`tests/test_release_scripts.py::test_nightly_two_runs_at_once_end_at_the_newer_commit`,
subcase `first='A'`:

```text
gh: Not Found (HTTP 404)
release_files.sh: nightly: SHA256SUMS still differs from the files after 5 uploads
```

The case name and message match the known release-script race signature.
That test and `scripts/release_files.sh` are unchanged from pinned main;
this is not a current-main rerun or a green-suite claim. The failure was
retained, not retried away. The pre-review revision had passed
`475/0/20` in 158 s; that earlier result is not a substitute for this
rerun. The 20 skips are the runner's existing Windows/game/GPU-only
declarations; none was added here.

## Real-room trial

One real request, made with this branch's client (`d22aec74`) for this
pull request: **T-6BH3**.

```text
$ python -I scripts/test_requests.py request --pr 2830 --summary "Test requests pin a commit (queue client)" --needs "..." --notes "..."
asked for testing as T-6BH3
commit to build: d22aec74f83702b20ad8346b5879b6a5fd30dacd
note: #2830 is not from a collaborator and has no "testing wanted" label, so nobody can claim it until a maintainer adds the label on GitHub
```

The card as this branch's client shows it (`show T-6BH3`):

```text
T-6BH3  OPEN  https://github.com/NotRexed/Hookshot/pull/2830  Test requests pin a commit (queue client)
    asked by you on 2026-11-12 15:37Z
    commit: d22aec74
    needs: Anyone using scripts/test_requests.py, Windows or Linux; no game needed
    waiting for a maintainer to add the "testing wanted" label on GitHub: not claimable yet
    Use the client from this PR: claim prints the commit to build; done records the commit tested. What to check is in the description.
```

The same card as main's client (`51da0464`) lists it: everything as
before, with the two new lines read as notes:

```text
T-6BH3  OPEN  https://github.com/NotRexed/Hookshot/pull/2830  Test requests pin a commit (queue client)
    asked by you on 2026-11-12 15:37Z
    needs: Anyone using scripts/test_requests.py, Windows or Linux; no game needed
    waiting for a maintainer to add the "testing wanted" label on GitHub: not claimable yet
    Use the client from this PR: claim prints the commit to build; done records the commit tested. What to check is in the description.
    Commit: d22aec74f83702b20ad8346b5879b6a5fd30dacd
    Head: d22aec74f83702b20ad8346b5879b6a5fd30dacd
```

**The Discord relay accepted the new message.** The card appeared in the
test-request channel like every other one (title, "Waiting for a
maintainer to add the testing wanted label", the Needs line, "Pull
request: #2830", T-6BH3). It does not show the commit, as expected: the
relay would need its own small change for that.

**Not shown yet:** a live `claim`, `done` and `update`. The card cannot
be claimed until a maintainer adds the label, so those three ran against
the test room only. @hckrman101, as the relay's owner: could you confirm
the relay is happy with the extra `Commit:` / `Head:` lines on claim and
result messages and with the new `update` action before this is relied
on? If it is not, the fallback in Risks (the same data in the event's
content, the visible text unchanged) is what I would do.

## Not tested

- Windows beyond the project's own source checks (they run this test
file on `windows-2022` and passed on the fork run above); the client was
not used against the live room from Windows.
- A live Matrix request/claim/result, Discord embed rendering, or the
relay's labelled-PR producer.
- The client's real GitHub HTTP path; its response parsing, token
headers and rate-limit behaviour were exercised with fakes.
- A game build or game run; no game code changed. No native C/C++
fixture was added or changed.

## Risks

The behaviour changes from main are limited to:

- New requests, claims and results carry additive commit/head lines and
render their commits. Pinning means a request needs a successful head
lookup or an explicit full SHA; a pinned result with no recorded claim
commit needs `--commit`. No approval rule is relaxed.
- The new requester-only `update` action moves the pin without deleting
history.
- `done` asks GitHub once to compare heads when it can; a failed lookup
is only a note and never blocks the result.
- Results for another or unknown commit do not count as tests of the
pinned commit or block a fresh claim for it.

Plain `list` keeps main's approval display and API-call count. The
relay's owner should confirm the card rendering before rollout.

- A strict relay could reject unexpected body lines, or accept them
without displaying the commit. If the real-room trial shows that, stop
rollout and agree on structured event-content metadata with the relay
owner while leaving the visible body byte-identical. The current change
implements appended lines, not that contingency.
- Old clients continue to work but cannot enforce pins or understand
`update`. A mixed-version queue can therefore still contain results with
unknown commits; these are shown as unknown, not silently assigned a
SHA.
- Explicit non-head SHAs are syntax-checked only. A wrong or ambiguous
SHA needs correction by the requester. “Earlier commit” means different
from the head observed then; a force-push need not preserve ancestry.
- A head can advance after any GitHub response. The recorded SHA states
exactly what that command observed; it is not a promise that the branch
will stay there.

## Overlaps

The overlap check found three open PRs sharing an edited file; their
shared hunks were read:

- **#2807:** a real conflict at `docs/test-requests.md:25–40` on this
branch (main's line 25). It adds the hardware-runner link to “You can
take part…”, immediately after this change's new CLI example. Keep both;
whichever lands second rebases. Its label-gated automation enforces the
exact labelled commit and checks it is still the PR head before running
the test: that is the automated workflow that needs an unambiguous
requested/tested commit in this queue. The conflicting hunk does not
implement the queue fields.
- **#2732:** development-pack advice in an earlier part of `AGENTS.md`;
merges cleanly with the request instructions.
- **#2561:** the opening documentation links and general rules in
`AGENTS.md`; merges cleanly with the request section.

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

---------

Co-authored-by: GPT-6 Astra <[email protected]>
Co-authored-by: NotRexed <[email protected]>

Ported to Longshot: pr_info reads the head and base from git.magicbean.dev's answer and keeps Longshot's writer check
(not GitHub's author_association). An explicit non-head pin is checked against the PR's own commits
(/pulls/<n>/commits) instead of GitHub's compare. The tests, docs and skills say git.magicbean.dev and forge.py.

Co-authored-by: Cursor <[email protected]>
**Result:** all **13 authored cases pass** on `NVIDIA GeForce RTX 4090`
(615.78.08), `Intel(R) Graphics (RPL-S)` (Mesa ANV 26.2.5), and Ubuntu's
llvmpipe (Mesa 25.2.8). Validation is directly confirmed for every
fixture on lavapipe in CI; it was not installed for the local
NVIDIA/Intel runs. The validated fork job took **23 seconds from start
to finish**, including setup.

**What this is for:** give contributors real Vulkan execution checks
without a game dump, following [#2797's idea of game-free GPU
tests](https://github.com/NotRexed/Hookshot/issues/2797#issuecomment-6826819619).
The existing fixtures use the production write-back shader and
copy/presentation helpers, but ordinary source checks did not execute
their device paths. **Not measured:** the game, a swapchain, HDR, DLSS,
full renderer behavior or performance. Lavapipe is CPU software Vulkan,
not a hardware GPU.

Refs #2797. This is only the hosted, dump-free part of that proposal. It
needs no exception to “No game in CI,” no self-hosted runner and no
access to a tester's machine.

Device selection is explicit in both places: CI pins the lavapipe ICD
and matches the reported name `llvmpipe`; local runs pin the
NVIDIA/Intel ICD and match `RTX 4090` / `RPL-S` from `vulkaninfo`, not a
marketing label.

## Problem

- `tests/run_all.py --strict` fails missing build tools, but retains
each test's declared OS skips. Its shards do not add GPU opt-ins.
- `test_wb_diff_observations_gpu.py` therefore only compiles.
`test_vulkan_copy_span.py` runs its CPU fixture unless asked for
`--gpu`. The offscreen aspect fixture is skipped on both CI systems.
- A zero exit alone would be misleading: the copy fixture returns zero
when it prints `SKIP`. Write-back and aspect also do not turn validation
messages into process failures themselves.
- The aspect fixture preferred any discrete device and had no explicit
selection option. That is unsuitable for claiming that a particular
driver was exercised.

## What changed

- **First commit, test-only:** aspect gains `--device SUBSTRING`,
case-insensitive. No match fails and lists the devices seen. Without the
flag its existing first-discrete-else-first preference and output stay
unchanged.
- **One new job in `source-regressions.yml`:** `Vulkan fixtures
(lavapipe, validation)`, on `ubuntu-24.04`. It installs only the C++
compiler, Mesa Vulkan driver, loader/headers, validation layer and
`vulkaninfo`; records their versions; and requires exactly one lavapipe
ICD manifest. Both Vulkan loader variables point to that one file.
Checkout remains commit-pinned, without persisted credentials.
Permissions remain read-only; existing concurrency applies.
- **`tests/run_vulkan_fixtures.py`:** records `vulkaninfo --summary`,
requires one llvmpipe device and the validation layer, then explicitly
selects llvmpipe in all three fixtures. Nonzero exits, exit 77, skips,
missing/wrong device names, missing/repeated cases, zero readback counts
and validation errors fail the job. It keeps running the other fixtures
after a failure and writes the usual result/time/check/note summary.
- **Validation:** forces the Khronos layer and requires the loader to
confirm insertion into each fixture's instance and device chains.
Installed/advertised validation alone is not enough. The two fixtures
without an error callback have the layer log errors to stdout; the
wrapper reads that output and fails on errors. The copy fixture must
also report that its validation messenger is enabled. No product code is
changed to hide errors.
- **Game-free regression and contributor guide:** cover the failure
rules, workflow pinning/permissions/device settings, and the actual
aspect-selection function with authored device properties. Ordinary
source checks keep their GPU-independent defaults.

| Fixture | Executed checks | Required Vulkan support |
|---|---|---|
| Write-back observations | Five buffer/feedback cases, using the
production compute shader | 1.3, buffer device address, maintenance4,
compute subgroup votes, coherent host memory |
| Copy span | Four texels and untouched padding in a 24-byte
upload/readback span | 1.1, transfer queue, RGBA8 transfer support,
coherent host memory |
| Offscreen aspect | Seven aspect/crop/blit cases with pixel readback |
1.3, synchronization2, RGBA8 linear blits, graphics queue |

These capabilities are present in [Mesa 25.2.8's lavapipe
declarations](https://gitlab.freedesktop.org/mesa/mesa/-/raw/mesa-25.2.8/src/gallium/frontends/lavapipe/lvp_device.c)
and [format
handling](https://gitlab.freedesktop.org/mesa/mesa/-/raw/mesa-25.2.8/src/gallium/frontends/lavapipe/lvp_formats.c),
the Mesa release currently listed for Ubuntu 24.04 updates. The actual
runner's package and device versions are logged. There is no reduced
“pass” set: all 13 cases are required; a missing capability fails.

**When it runs:** every PR to main, main push and manual source-check
run, matching the existing unfiltered source workflow. Packaging jobs
ignore documentation-only changes and the large DLSS link job has narrow
filters; this small authored execution check stays with source
regressions. It also notices changes in the runner's Mesa on otherwise
unrelated PRs.

## How it was tested

Original runtime base `51da0464b4`; final
rebased base **`f5c9acc15b`**, head
**`d5d04b20a36433e9ff1de18acdf95ae0581eb879`**. All four commits are
patch-identical across the rebase. The fixture drivers/templates, shader
and copy inputs are unchanged; the exact generated aspect program is
byte-identical despite unrelated changes in its source file. Hardware
results therefore carry without repeating the same programs. The final
docstring-only commit changes no executable code.

Linux kernel `7.2.15-1-omarchy`, i9-13900K, Clang 23.1.3, GCC 16.2.1,
Python 3.12.16, Vulkan headers 1.4.345; 2026-11-12. All fixture
execution is offscreen, without a window or swapchain. Final
author/trailer/title/no-game-output checks pass on the rebased head.

- New regression, with Clang: first 10 checks pass; after requiring
direct layer-activation evidence, `Ran 11 tests in 0.213s`, `OK`. The
added case rejects either missing activation marker from every fixture.
- Same regression against the pinned main source: exits 1, for the
absent explicit selector and absent lavapipe job. The output/validation
rejection cases are device-free; they do not count as GPU execution.
- Original-base whole source suite on `d7760411`: `476 passed, 0 failed,
20 skipped in 213 s (linux, 4 at once)`, with `--strict --jobs 4`; no
shard.
- **Rebased whole suite on d5d04b20: `501 passed, 2 failed, 20 skipped
in 294 s (linux, 4 at once)`.** The new wrapper test passes. The two
failures are main's own at this base, not this change's:
`test_program_cache_one_variant.py` (fixed by #2861) and
`test_readme_menu_rows.py` (fixed by #2857). On Windows main has a
third, `test_wb_shadow_remake.py` (a fix is on its way as its own pull
request). [The fork's run on this rebased
head](https://github.com/mooki000/Hookshot/actions/runs/43271225010)
fails in exactly those three tests and nowhere else; its `Vulkan
fixtures (lavapipe, validation)` job, both Windows builds, the AppImage
and actionlint pass.
- Windows-target syntax: both the new selector-test translation unit and
the full generated aspect fixture pass with their warnings-as-errors
flags. The automatic checker intentionally marked the GPU-skipped driver
unjudged; an explicit compile-only capture of that same driver completed
the syntax check. Neither is a Windows device run.
- Real Windows source execution on `5857491d`:
`test_vulkan_fixtures_ci.py` passes in part 2/2 (1.3 s), including the
selector helper. Windows totals: part 1/2 `227 passed, 0 failed, 17
skipped in 209 s`; part 2/2 `236 passed, 0 failed, 16 skipped in 165 s`.
Full Windows Vulkan execution remains untested.
- **RTX 4090 / NVIDIA 615.78.08 / Vulkan 1.4.351:** all 13 cases pass,
all three commands exit 0; write-back 0.749 s, copy 0.617 s, aspect
4.566 s, including compilation. Runs at 10:41:26–10:41:31 -06:00;
1-minute load 7.03→8.07. **Validation layer not installed here**. No
Xid, device-loss or NVRM errors. The nine HDMI-0 EDID-read kernel
warnings are unrelated display messages, not validation errors.
- **Intel(R) Graphics (RPL-S) / Mesa ANV 26.2.5-arch1.1 / Vulkan
1.4.354:** all 13 cases pass with substring `RPL-S`, all commands exit
0; write-back 0.669 s, copy 0.901 s, aspect 0.963 s, including
compilation. Runs at 11:32:02–11:32:05 -06:00; load 121.24/115.20/76.38,
unchanged during the three commands. These are correctness results on a
busy PC, not a speed comparison. **Validation layer not installed
here**; kernel interval empty.
- **A setup mistake of mine, kept for the record:** the first Intel
attempt asked for `UHD Graphics 770`, which is not in the name the
driver reports, so the write-back fixture returned 77 with zero cases
run. That is the wrapper refusing a wrong device, as intended; the Intel
cases were then run with `RPL-S`.
- [The fork's source workflow on the patch-identical head
`6085c658`](https://github.com/mooki000/Hookshot/actions/runs/43270742409),
on the base from before main's three failures: every job passes, both
Windows parts included, and the Vulkan job confirms the Khronos layer in
each fixture's instance and device chains. Whole job: 23 to 26 seconds
in the earlier runs, 11 of them package setup.

The [first lavapipe
job](https://github.com/mooki000/Hookshot/actions/runs/43255289863/job/131311732104)
selected **llvmpipe (LLVM 20.1.2, 256 bits)**, API **1.4.318**, Mesa
**25.2.8-0ubuntu0.24.04.4**, with validation package **1.3.275.0-1**,
loader/headers **1.3.275.0-1build1**, Vulkan tools
**1.3.275.0+dfsg1-1**, GCC **13.3.0** and Python **3.12.3**.

| Fixture | Cases passed in each run | First compile + execute |
Validated compile + execute |
|---|---:|---:|---:|
| Write-back | 5 | 1.77 s | 1.52 s |
| Copy span | 1 | 0.87 s | 0.86 s |
| Offscreen aspect | 7 | 1.07 s | 0.97 s |

The validated result is **`3 passed, 0 failed, 0 skipped in 6.30 s
(lavapipe, validation on)`**.

No local lavapipe execution or validation-layer result is claimed for
these runs: the software ICD was absent when checked, and the Khronos
layer was unavailable to both local driver runs. No system packages were
installed for this work. The fork job supplies the lavapipe/validation
proof.

## Not tested

- No game or game-derived fixture data, full renderer startup/replay,
swapchain, display, HDR, DLSS or frame generation. The aspect fixture
still stubs filtered/HDR routing.
- No AMD/Steam Deck or Windows Vulkan execution. The new selector's
Windows syntax check is not a Windows device run.
- No frame-rate or hardware-performance comparison. Software Vulkan
cannot establish real-GPU correctness or speed.
- No extra synchronization-validation or GPU-assisted-validation
coverage is claimed; the job enables ordinary core validation.

## Risks

- An Ubuntu Mesa, LLVM or validation-layer update can change a result.
Versions and full error messages are kept so that a runner change can be
distinguished from a source change. Package versions are recorded, not
pinned to an apt revision that may disappear.
- One more concurrent job per source-check run. The two whole jobs took
26 and 23 seconds, below the approximately-one-minute target; later
runner/package changes can move that. The five-minute timeout is a
safety limit, not an expected duration.
- A renamed or additional fixture case requires updating the strict
expected-case list. A missing case must never look like a successful
run.

## Overlaps

- [#2807](https://github.com/NotRexed/Hookshot/pull/2807) is the
separate label-gated **self-hosted** hardware proposal. Its description,
review, proposed lint configuration and workflow test were read. This
change does not touch its files, register a runner or adopt its
game/sandbox path.
- The refreshed overlap check on the rebased branch found eight other
open PRs sharing these paths. Source-workflow hunks in #1025 (MCP
packages), #1872 (macOS checks), #2373 (Windows launcher tests) and
#2739 (.NET/launcher builds) were read; all four merge cleanly with this
branch. None adds this job.
- **Documentation conflict with #2561:** its rewrite removes the
contributor-guide testing section containing the new paragraph; the
actual conflict hunk was read. Keep the paragraph in its relocated
`docs/dev/testing.md` when the two changes meet. Other contributor-guide
hunks read: #2820's license list, #2317's data/cache paths, #2201's
version-policy guidance; #2274 also touches only the license list here.
- Arkex-ai's ASTC fixture from #2829 also passed a separate
lavapipe-with-validation trial today and is intended to join this job
after both changes merge; no ASTC code is included here.

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

---------

Co-authored-by: GPT-6 Astra <[email protected]>
Co-authored-by: NotRexed <[email protected]>

Ported to Longshot: the job is in .forgejo/workflows/ci.yml and, like the other full checks there, runs when CI is
started by hand (workflow_dispatch), not on every push.

Co-authored-by: Cursor <[email protected]>
test: one source cutter for the checks (tests/srccut.py), 80 checks moved to it (#2914)
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
17380e5cfb
123 checks carry their own copy of a brace counter (`def block`) to cut
a function out of a source file. The copies count braces inside strings,
character literals and comments too, and when a function is renamed or
reshaped they stop with a bare `ValueError` or `IndexError` that doesn't
say which source or marker: the trap AGENTS.md warns about ("Tests read
the source"). This gives the checks one cutter and moves 80 of them to
it, each shown to cut exactly what it cut before. The dev-loop
proposal's D2 (#2636).

## Change

- **`tests/srccut.py` (new, standard library):**
- `block(text, marker)`: from the marker to the brace that closes the
first `{` after it, counting braces outside string and character
literals (raw strings too, and C++14 digit separators), comments and
preprocessor lines;
  - `between(text, start, end)`;
- a marker that is missing, or that appears twice, stops the check with
one message naming the source and the marker (`unique=False` takes the
first on purpose);
- `SRCCUT_CHECK_NAIVE=1` also cuts each block the old way and stops if
the two differ: what a migration runs to show it changed nothing.
- **80 checks import it** in place of their copy, one commit per source
file they read (`vk_menu.cpp`, `vk_draw.cpp`, `vk_memory.cpp`,
`vk_texture.cpp`, `hookshot_env.h`, `hos_main.c`, ...). Only exact
copies of the plain counter moved.
- **Not moved:**
- three checks whose `block` isn't the plain counter (a start offset;
positions returned): restored as they were;
- 13 checks whose marker appears more than once in the source (a
declaration before the definition, for example). The old counter took
the first one silently, so each needs a look rather than `unique=False`;
- 51 checks with other cutters (`function`, `body`, `between`, ...):
later, by source file.

## Testing

- **Every moved check, against today's `main`:** each run as `main` has
it and as moved with `SRCCUT_CHECK_NAIVE=1`, with its declared tools
(the Builder's clang, the Vulkan headers): **80 of 80 pass both ways**,
no cut differs.
- **`tests/test_srccut.py` (new):** braces in strings, character
literals, raw strings, comments and preprocessor lines; a fixture the
old counter cut wrongly; a missing marker and a repeated one name the
source; `between`; an unclosed block; the comparison with the old
counter.
- A follow-up after #2887: `run_all.py --changed` runs a test helper's
importers (srccut's) rather than every check.

See #2636 (D2).

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: NotRexed <[email protected]>

Ported to Longshot: test_builder_update_back.py and test_linux_update_skip.py are left out (Longshot removed them with
the Builder window, #181).

Co-authored-by: Cursor <[email protected]>
Fixes the crash in #2941 (v2.2.0-rc.1): the game closes every time on
the load into Hyrule Field.

**What the crash was.** I built rc.1's sources with DLSS here, using the
same llvm-mingw 20261104, the same `x86-64-v3` target and the NGX SDK
the reporter's builder had. With that build's `hookshot_vk.map`, the
reporter's backtrace reads:

```
KERNELBASE RaiseException  <- libunwind/libc++ throw
  mkwvk::memory_table_flush() +0xf2             (vk_memory.cpp)
  mkwvk::upload_command_buffer() +0x28
  flush_ub_updates +0x109 <- replay +0x3ac4 <- run_submit <- resume_queue <- schedule <- submit_job <- submit_q
  hookshot.exe (the game's lifted code)
```

8 s later the log shows `0xc00000ff` (bad function table): the exception
got as far as the game's code and couldn't be unwound.

- **Exception code** `0x20474343` is a C++ exception.
- **What threw:** the first thing `memory_table_flush` does is build its
list of page changes, so the throw is `std::bad_alloc`.
- **Why there was no memory:** the reporter has 31.9 GB of RAM with a
2.1 GB page file, so the commit limit is about 34 GB. Hookshot was at
24.3 GB of private commit as Hyrule Field loaded.

This is the same limit as #2972, reached through the renderer's own heap
rather than guest memory.

**Changes**
- **`vk_memory.cpp`:** `memory_table_flush` reserves its list in one
allocation. If that throws, it writes the changes into the mapped table
at once, as `RX_VK_TABLE_NOW=1` does, and logs it (8 times at most). The
cost of that path is that work submitted before may read the new entries
early. Before this change, the game closed.
- **`vk_texture.cpp`:** the renderer now holds 128 MB of commit as an
emergency reserve while there is room. On the first allocation that
fails, `operator new`'s handler gives the reserve back and the
allocation is tried again.
- On the next pass, #2972's commit rule sees the headroom gone and
shrinks the textures.
- Once headroom is back above the rule's reserve plus 128 MB, the
reserve is taken again.
- Waiting wouldn't help here, because the thread that runs short is the
presenting thread, the same one that trims.
- Windows only. `RX_VK_COMMIT_RESERVE_MB=0` turns it off along with the
rule.
- **`tests/test_commit_limit.py`:** covers both.

**Checked so far**
- `test_commit_limit.py` passes.
- `vk_texture.cpp` compiles syntax-only with the build's flags.
- An in-game route run on this branch is next.

The failed-allocation path itself can't be triggered here without
exhausting this PC's commit, so only the test covers it.

**Still wise for the reporter:** let Windows manage the page file. A
fixed 2 GB page file leaves about 10 GB of headroom on a 32 GB PC.

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
**What this is for:** the Deku Tree floor web does not react when Link
jumps on it above 60 fps. This restores its movement in the Linux/RTX
4090 test; it is not a fix for every above-60 issue.

Fixes #2667. Refs #2271 and #1742.

## Cause and change
`f_80652cb0` truncates elapsed time divided by 1/60 to whole simulation
steps, so a sub-one update becomes zero.
The existing fractional carry now retains that remainder per web object
before the unchanged 0..3 clamp.
One `WHOLE_SITES` entry in `nx2/oot_patches.py`; no new runtime helper,
switch or logging. The separate Scarecrow's Song fix touches neighboring
table lines, so this stays minimal and independent.

## Evidence
2026-11-12, Arch Linux 7.2.15, i9-13900K, RTX 4090 / 615.78.08,
windowless; 1280×720, game's own 50% render scale, frame generation off.
Three jumps per cap. Before: main `9b6072cd` with local entry tracing
(not part of this change); fixed: this change on `9b6072cd`, without
that tracing.

| Build | Cap | Achieved presents/s | Below 16.7 ms | Observed updates |
Steps delivered | Four-height vectors |
|---|---:|---:|---:|---:|---:|---:|
| Before | 60 | 59.996 | 52.04% | 661 | 661 | 274 |
| Fixed | 60 | 59.937 | 50.38% | 660 | 660 | 270 |
| Before | 240 | 87.981 | 99.59% | 969 | 0 | 1 |
| Fixed | 240 | 89.290 | 99.90% | 983 | 660 | 492 |

At the fixed 240 cap, 323 zero-step and 660 one-step updates delivered
59.95 steps/s. The four central heights changed from -1.0261 to -0.0681,
rather than staying frozen.
![Before the change. 1 and 2: cap 60, Link on the web and during a jump.
3 and 4: cap 240, the web keeps one
shape](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/66e0075b945eb968da1d7248bd6712ef8ea81540/139-spider-web-before.jpg)

![With the change. 1 and 2: cap 60. 3 and 4: cap 240, the web bends
under Link and rises during the
jump](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/66e0075b945eb968da1d7248bd6712ef8ea81540/140-spider-web-fixed.jpg)

In stills the movement is small (the dip under Link and the strands
lifting during the jump); the step and height numbers above are the
proof, read from the web's own grid so that camera movement cannot be
mistaken for it.
The authored fixture fails main's 120/240-Hz totals; GCC 16.2 and Clang
23.1 pass (`PASS: web floor steps`), including separate objects, clamps
and a long pause. Targeted checks: `3 passed, 0 failed, 0 skipped`.
Windows-target syntax: one C unit passes. Both Windows and Ubuntu
source-check parts [passed on my
fork](https://github.com/mooki000/Hookshot/actions/runs/43292980413) for
the code commit; it was then rebased onto main `4dd972a8` without change
(range-diff equal), with the docs row added on top.
Two earlier setups stopped before the jumps because of wrong assumptions
in my measurement script, not because of the game. One high-cap attempt
while the PC was busy stayed below 60 fps and was discarded before its
counters were read; the final pair ran with nothing else on the GPU and
both games exited without faults. These rates establish the test
condition, not a speed gain.

## Not tested / risks
Intel, AMD and Windows gameplay; other interactive floors such as the
flying carpet; long play or web-breaking progression. The updater is
shared by interactive floors, so those still need coverage. Above-60
play remains experimental.

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

---------

Co-authored-by: GPT-6 Astra <[email protected]>
Fixes #2885

Some controls have two keys by default, but the Controls page showed a
control's keys as one Keyboard cell, and binding it wrote the new key to
the first slot and cleared the second. After any rebind the control kept
one key, and there was no way to give it a second one.

The host already keeps two key slots per control, so the page now shows
them as two cells, Key 1 and Key 2, next to the two gamepad inputs.
Binding or clearing a cell changes its own slot and leaves the other key
alone.

- Left and Right move through all four cells. On the keys-only rows (the
stick and camera directions) they move between the two key cells.
- A key taken from another control is still removed from it, as before.
- The options file's `key_<button>` lines are read and written as
before, so existing files load unchanged.
- The names column is narrower. Each key cell is as wide as "Right
click" needs (between a sixth and a fifth of the row), and the gamepad
cells share the rest. At Small and Medium the key names show whole; at
Large, where main already cut the gamepad names, the long ones are cut
too, and scroll when selected as before.
- "Key 1" and "Key 2" use the existing "Key %d" translations.

### Testing

- `tests/test_controls_key_slots.py` (new): cuts the page's key binding
and clearing code from `vk_menu.cpp` and runs it against a host that
records the bindings. Binding Key 2 keeps Key 1, binding Key 1 keeps Key
2, clearing one keeps the other, a keys-only row binds Key 1. It fails
on main (the second key is cleared).
- `tests/test_menu_hit.cpp`: the fourth cell's hit area, and the column
rules (`ctl_key_slot`, `ctl_column`, `ctl_step`).
- `tests/run_all.py` on Linux: 443 passed, 85 skipped, 1 failed
(`test_bake_portability.py`, no `zstandard` module on this machine; it
fails the same way on main).
- Linux build on an RTX 5060 Ti, headless with `RX_VK_SHOT_MENU=1` and
`RX_VK_MENU_SCRIPT`, 1920x1080, the menu opened before the first
picture: the page at each Menu size, before (main 4dd972a8) and after,
with Plus (Tab and Enter by default) selected in its keyboard cell.
Binding a key on the page was not done in the game; the test above
covers it.

| Menu size | Before | After |
|---|---|---|
| Small | ![before,
Small](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/m-small.webp)
| ![after,
Small](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/k-small.webp)
|
| Medium | ![before,
Medium](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/m-medium.webp)
| ![after,
Medium](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/k-medium.webp)
|
| Large | ![before,
Large](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/m-large.webp)
| ![after,
Large](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/k-large.webp)
|
With "controller in the background" on (`HOS_INPUT_BACKGROUND=1` or the
menu's row), the pads reach the game behind another window, but an
ocarina note bound to a custom pad button did not: `oc_pad_word`
returned the game's word unchanged whenever the window was not in front,
so the remapped notes were dropped while the rest of the pad worked.

## Change

- `oc_pad_word` gives the notes in the background when the pads count
there, by the same rule `read_input` uses (`pads_in_background` on the
switch, else the menu's row), through a small `pads_behind` helper.
- `oc_notes` takes the keyboard's note keys only with the window in
front, so a key held in another program never plays a note. The pads'
note buttons count in both cases.

## Testing

- `tests/test_ocarina_notes.py`, new case: with the pads counting in the
background, a pad's note button plays its note and the keyboard's note
key does not; without the row the word is unchanged. It fails on main
(`oc_pad_word(0)` gives no L), and passes here.
- `tests/test_input_background.py`: its fixture takes the new helper.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
- Not tested in the game.
With the native mouse camera, `hos_mouse_camera` skipped the mouse's
motion while the mouse was released (Esc, a menu open, or the window
losing focus), but it still wrote its last yaw and pitch back into the
camera every frame. The game's own camera work, such as swinging behind
Link, was undone until the mouse was captured again or the stick moved.

## Change

In `host/mouse_native.c`, `hos_mouse_camera` asks `hos_mouse_take`
first. With the mouse released it forgets its angles and leaves the
camera alone, so the next captured frame starts from the game's angles,
the same way it already does after the stick turns the camera.

## Testing

- `tests/test_mouse_native.py`, new case: with capture off the game's
yaw (10) is kept, and once the mouse is captured again a turn starts
from it (11). It fails on main and passes here.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
- Not tested in the game.
"Report a bug" in the Insert menu checked report.txt's write by
`fwrite`'s count only and dropped `fclose`'s result. On a full disk the
write only fills the stream's buffer and succeeds; the close is what
fails. The menu then showed the report as saved, and the folder held a
cut or empty report.txt.

## Change

`bug_report::write` in `gpu/vk/vk_bug_report.cpp` counts a failed
`fclose` as a failed write, so the report ends with the existing
"report.txt could not be written" reason.

## Testing

- `tests/test_bug_report.py`, new case (Linux, where `/dev/full`
exists): the driver points the report folder's report.txt at `/dev/full`
before it is written; the report is not shown as saved and the log gives
the reason. It fails on main (the report shows as saved) and passes
here.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
- Not tested on Windows.
With AMD FSR 4's shader tuning (#2798), a tuned pipeline reads its
weight tables from a tail at the end of the library's storage buffer,
and `tune::cmd_dispatch` writes that tail before the first dispatch that
reads it. The unrolled pipeline is built by a background job and
published into `better` from its own thread. `cmd_bind_pipeline` binds
the library's pipeline when no unrolled one exists yet, but
`cmd_dispatch` asked `better` again at dispatch time. When the job
published between the two, the dispatch took the unrolled pipeline as
bound: it skipped the tail's upload (`due` stayed set) and the fallback
to AMD's own pipeline, while the tail-reading pipeline ran once on a
tail not written yet.

## Change

`bound` keeps, per command buffer, the library's pipeline and whether
the bind put the unrolled one in its place (`struct Bind`).
`cmd_dispatch` decides from that, not from `better` at dispatch time.
The twin path still gets the library's pipeline from it. No lock is held
across a driver call that was not held before.

## Testing

- `tests/test_fsr4_bind_dispatch.py` (new): cuts `cmd_bind_pipeline`,
`cmd_dispatch` and the structs they use out of `vk_fsr4.cpp` and runs
them against recording stand-ins for the driver's calls, with no GPU and
no `spirv-val`. Three cases: the library's pipeline bound and the
unrolled one published before the dispatch (the tail is written and
`due` cleared); the unrolled one bound (no tail written); a tail that
cannot be read with the unrolled one published in between (AMD's own
pipeline is bound). The first and third fail on main and pass here.
- Linux renderer built with `HOOKSHOT_FSR=1`: `vk_fsr4.cpp` compiles.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- Not run on an RDNA 4 card.
On Linux, `keys_update` in `host/posix/display_sdl.c` turned three of
SDL's five mouse buttons into virtual keys. The Controls page offers
Mouse 4 and Mouse 5 (0x05 and 0x06), and `host/nn_hid.c` reads them, but
on Linux the listener never saw a side button, and a saved binding to
one (`key_zl=5,0`) never acted. Windows has them.

## Change

- `keys_update` also sets `VK_XBUTTON1` from SDL's X1 bit (8) and
`VK_XBUTTON2` from X2 (16). The bits are the same in SDL 2 and SDL 3,
and `hos_sdl_mouse_buttons` passes SDL's mask through unchanged.
- `host/posix/windows.h` defines `VK_XBUTTON1` and `VK_XBUTTON2` beside
the other mouse keys (`nn_hid.c`'s own definitions are under `#ifndef`).
- The window guard around those lines stays.

## Testing

- `tests/test_sdl_mouse_buttons.py` (new): `keys_update` and the posix
virtual keys cut from the sources, built with stand-ins for SDL. Each of
SDL's five button bits sets only its own key (5 and 6 for the side
buttons), both side buttons together set both, and nothing is set
without a window. It fails on main (keys 5 and 6 stay 0) and passes
here.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- No side button was pressed in the game on Linux.
With motion controls on, opening the Insert menu holds back a pad's
buttons and sticks (`read_input`), but `GetSixAxisSensorStates` only
asked about the window and the "Controller in background" row. Moving
the controller while in the menu still aimed in the game.

## Change

`motion_counts` in `host/nn_hid.c` takes the menu's state (`rx_option(1,
0)`) as well, and the motion counts only while the menu is closed. With
the menu open the handler gives its existing at-rest state; the switch
between live and at rest already renumbers the samples.

## Testing

- `tests/test_motion_background.py`: three new cases (menu open in
front; open behind another window with "Controller in background" on;
open with `HOS_INPUT_BACKGROUND=1`) give the pad at rest; the six cases
from before are unchanged. On main the handler never looks at the menu
(`motion_counts` has no parameter for it).
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- No controller was moved in the game.
Saving the options (any row changed in the menu) opened
`hookshot_options.ini` with `"w"`, which empties the file before the
first line is written. When the write then failed, on a full disk for
example, the player was left with an empty or cut options file, and the
next start went back to the defaults.

## Change

`save()` in `gpu/vk/vk_menu.cpp` writes the options to
`hookshot_options.ini.new` beside the file, checks the writes and the
close, and only then moves the new file over the old one (`rename` on
Linux, `MoveFileExA` on Windows). A save that fails removes the part
file, leaves the options as they were, and logs one line.

- An options file that is a link keeps working as before: on Linux the
link's target is written. A link that cannot be resolved here (one to a
file not made yet, or any link on Windows) is written through in place
as before, so it stays a link.
- #2430 changes the same `fopen` so the game does not write through a
link at all. Whichever lands second can take the other's rule for links;
the part-file step does not depend on it.
- `tests/test_mesh_mode_switch.py` looked for the old
`fopen(ini_path().c_str(), "w")` call to find the end of the save; it
now looks for the new one.

## Testing

- `tests/test_menu_env_overrides.py`, new Linux cases:
- A save that cannot write a byte (`RLIMIT_FSIZE` set to 0): the file is
unchanged, no `.new` is left, one line is logged, and the next save
writes. This fails on main (the file is emptied) and passes here.
- The options file as a link, first to a missing file, then to an
existing one: it stays a link and its target holds the save.
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- The Linux renderer builds with the change.
- Not built or tested on Windows.
On Linux, `waveOutClose` (`host/posix/win32_posix.c`) only emptied the
queue: the SDL device that `hos_sdl_audio_open` (`host/hos_sdl.c`)
opened was never closed, and its id was dropped, so nothing could close
it. The game opens the output again when it takes no sound for 1.5 s
(`output_reopen`) and when the default device changes (`output_move`).
The first device was still open then, and where the default output takes
one open at a time the new open was refused and the sound stayed off for
the rest of the run. Elsewhere each reopen left another device and its
callback running.

## Change

- `hos_sdl_audio_open` returns what it opened (NULL when it did not
open) instead of 1 or 0, and the new `hos_sdl_audio_close` closes it:
SDL2's device (`SDL_CloseAudioDevice`), or SDL3's stream with its device
(`SDL_DestroyAudioStream`). The pull callback is not called once it
returns. An SDL3 stream that opened but did not resume is destroyed too.
- `waveOutClose` closes that device first, then empties the queue and
frees the rest.
- Windows plays through WinMM and is unchanged.

## Testing

- New `tests/test_sdl_audio_reopen.py` (Linux): builds the real
`waveOut` calls and SDL adapter over the system's SDL with
`SDL_AUDIODRIVER=dummy`, then opens the output, plays a block and closes
it, four times. On main the second open is refused; here all four open
and take their block. It ran against SDL2 2.32 (no SDL3 is installed on
this machine). It skips where neither library is installed.
- `tests/test_sdl_audio_library.py` follows the new return type.
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No real sound device or game was run, and the SDL3 path was not run.
On a Windows whose system messages are not plain ASCII (French, for
one), `tests/test_bug_report.py` fails on `main` before any change: when
a report folder cannot be made, its driver gets Windows' reason in the
system's code page, not UTF-8. The game itself already gets UTF-8 there,
through its manifest; this makes the reason UTF-8 without it too.

Fixes #2954

**Cause.** `make_dir` (gpu/vk/vk_bug_report.cpp) read the reason with
`FormatMessageA`, which answers in the process's ANSI code page.
`hookshot.exe`'s manifest sets that code page to UTF-8
(`nx2/win_manifest.py`, Windows 10 1903 and newer), so the game's log
and menu got UTF-8. The test's driver is the same source built without
the manifest, and got code page 1252 on a French Windows. So would the
game on a Windows older than 1903. (#2954 first said the game was
affected on any such Windows; [its
comment](https://github.com/NotRexed/Hookshot/issues/2954#issuecomment-6838019529)
corrects that, and its score, to 0.01.)

**Fix.** `make_dir` asks for the wide text (`FormatMessageW`) and turns
it into UTF-8 itself (`WideCharToMultiByte(CP_UTF8)`), so the reason
does not depend on the manifest. The error number and the trimming are
unchanged, and with the manifest, or in English, the bytes are what they
were. Linking the manifest into the test's driver would also turn the
test green, but it would leave `make_dir` depending on the manifest,
which a Windows older than 1903 does not read.

The line of the test's step 7 on a French Windows 11 (code page 1252),
`... (Windows error 3: Le chemin d’accès spécifié est introuvable.)`,
bytes of `d’accès`:

| | without the manifest (code page 1252) | linked with the game's
manifest (code page 65001) |
|---|---|---|
| before (`FormatMessageA`) | `64 92 61 63 63 e8 73` | `64 e2 80 99 61
63 63 c3 a8 73` |
| after (`FormatMessageW`, then `WideCharToMultiByte(CP_UTF8)`) | `64 e2
80 99 61 63 63 c3 a8 73` | `64 e2 80 99 61 63 63 c3 a8 73` |

The left column is also what the test's driver says before and after.
The right column is from a small program that makes the same calls for
error 3, built with the Builder's llvm-mingw and linked with the `.res`
that `nx2/win_manifest.py` writes; the game was not built or run for it.

**Test.** `tests/test_bug_report.py`:

- `run()` reads the driver's output as bytes and decodes it as UTF-8
itself, so a line in another encoding is named in the failure instead of
ending in `subprocess`'s reader thread (`UnicodeDecodeError`, then
`'NoneType' object has no attribute 'splitlines'`).
- `wiring()` holds `make_dir` to `FormatMessageW` and
`WideCharToMultiByte(CP_UTF8, ...)`, with no `FormatMessageA`. CI's
Windows is in English, where step 7's message is ASCII either way, so
this source check is what fails there without the fix.

Without the fix, on this PC: `bug report: make_dir's reason on Windows
is not Windows' wide text turned into UTF-8`, and with that check taken
out, `bug report: the driver (plain) said a line that is not UTF-8:
b'LOG menu: bug report not written: ... (Windows error 3: Le chemin
d\x92acc\xe8s sp\xe9cifi\xe9 est introuvable.)'`.

`python -I -B tests/run_all.py` on this commit, on Windows 11 in French:
484 passed, 1 failed, 43 skipped. `main` (e0e60920) gives 483 passed, 2
failed there: the one that passes now is `tests/test_bug_report.py`, and
the one still failing, on both, is `nx2/test_nup_paths.py`, which makes
symbolic links that this account is not allowed to make (WinError 1314);
this change does not touch it.

**Not tested in the game, and nothing to see there on a current
Windows.** With the manifest the bytes are the same before and after
(the table's right column, measured with the small program, not in the
game), so a run on Windows 10 1903 or newer, this PC included, would
show the same text either way. The difference would show only on a
Windows 10 older than 1903 in a language other than English, with a
`bug-reports` folder that cannot be made; I have no such PC. (The
control bridge, which my unattended runs go through, cannot press the
Bug report row either: it sets rows that have a value, and this row is
an action.)

**Overlap.** #2948 changes `bug_report::write` in the same source file
and adds a step after step 7 in the same test. The two do not touch the
same lines, and this branch merges with #2948's head as it is now
without a conflict (`git merge-tree`).

`docs/worklist.md`: one row, score 0.01 (S3 x 0.01 x 1.0).

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

Co-authored-by: Claude Fable 5.2 <[email protected]>
With `RX_VK_PREPARE_BACKGROUND=1`, an update builds what the notes name
before the game and the rest behind it. A start with console progress
(`started_up` in `gpu/vk/vk_main.cpp`) makes the shipped state list's
pipelines first: `if (update) state_list_run();` before
`precompile_run`. The preparing screen's worker (`prepare_with_screen`)
has no such call, so a start that shows the screen left those pipelines
out, and they were built when first drawn, in play.

## Change

The screen's worker calls `state_list_run()` for an update at the same
point, before `precompile_run`, inside its `!prepare_stopped()` check. A
stopped preparation still skips it. Nothing is added to the screen:
`state_list_run` takes no progress callback.

## Testing

- New `tests/test_prepare_screen_sequence.py`: both step sequences are
cut from the source and run against stand-ins that record each call. For
an update, both make the state list once, before `precompile_run`, then
start the background pass. When it is not an update, neither makes the
list. A stopped screen worker makes neither the list nor the precompile.
On main the screen's update sequence has no state list; here it passes.
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No game ran.
A crash or a full disk while the game writes
`hookshot_pipeline_states.bin` can leave it ending in part of a record.
`state_open_locked` in `gpu/vk/vk_draw.cpp` reads the whole records,
sets `keep`, and opens the file with `"ab"`, so every later run added
its new pipelines after the broken end. They were never read back: those
pipelines were built again in play at each start, and "Save my pipeline
list" sent a list whose end was garbage. `notes_open_locked` does the
same with the pipeline notes, also on its `known` path, where the file
is not read at all.

## Change

A small `cut_part_record(path, header, record)` cuts the file back to
its last whole record with `std::filesystem::resize_file` and logs it.
Both opens call it before appending: the state list when it keeps the
list, the notes when the stamp and record size match. The whole records
stay; a list that ends on a record's edge is left alone, and one shorter
than its header starts new as before.

## Testing

- New `tests/test_state_list_part_record.py`: `state_open_locked`,
`notes_open_locked` and the helper are cut from the source and run on
lists of their own: one whole record followed by 0 bytes, 1 byte, and a
record's size less one of the next. A new record is added as play adds
one, then the file is read back: both records there, the file a whole
number of records. The notes the same, with their records read before
the open and without. On main the part-record cases fail; here they
pass.
- `tests/test_pipeline_state_computes.py` compiles `state_open_locked`
too; its fixture now takes the helper along.
- Linux renderer built (`hookshot_vk.so`).
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No game ran; not built on Windows.
When the GPU driver stops answering during the build's shader
preparation (`prepare_watchdog`, exit status 4), the builder printed
"the shaders were not prepared now (the GPU driver stopped responding):
the game prepares them at its first start". The game does not do that.
The stopped run leaves `hookshot_prepare_skipped` beside the game, and
every start skips the preparation while it is there, so each shader is
built when it is first drawn, until a later build's preparation runs
through and removes the file.

## Change

- `nx2/make_game.py`: `not_prepared` takes the ending as an argument,
defaulting to the old one (`FIRST_START`). Exit 4 says "the game builds
each shader when it first draws it, and the next build or update tries
the preparation again". The other reasons keep their text, and the
marker's behaviour stays as it is.
- The three builders read the ending of the line instead of assuming it:
- Windows builder (`ShaderStep.Note`): the row says "Left to the game's
first start: why" as before, or "Not prepared: why. Then what happens".
- Python builder (`window.py`) and Linux builder
(`hookshot_builder.py`): the same, in each one's own wording.
- `tests/builder_workers/Program.cs` follows the rename.

## Testing

- `tests/test_prepare_at_build.py`, new cases:
- make_game: exit 4's line does not promise the first start and names
first drawn. Exit 3, a silent run and another status keep "at its first
start".
  - Both Python builders: the row text for each kind of line.
  - The new cases fail on main and pass here.
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- The C# builder and `tests/builder_workers` were not compiled: there is
no .NET SDK on this machine.
- No game ran.

Ported to Longshot: make_game.py's line and its check only. Longshot's builders are command line only (#171, #181), so
the builder windows' rows and their checks are left out.

Co-authored-by: Cursor <[email protected]>
On a card that is not NVIDIA (and with `RX_VK_NO_F16`, or without
`shaderFloat16`), `translate_into` in `gpu/vk/vk_shader.cpp` leaves the
movie upscaler's networks out on purpose: a linear upscale takes their
place (`upscale_stub`). It threw a plain `std::runtime_error`, so its
one catch counted each of them in `st_failed` and logged it as "vk:
program ... not translated: a network left out on this card". Those
lines used up the 30 the catch allows for real failures (#2943 is
exactly 30 of them), so a program that really failed to translate after
them was never named. The "prepared N shader programs" line and the
statistics lines also counted the networks as failed.

## Change

- The left-out case throws `LeftOut` (a `std::runtime_error`, same
text). The throw stays one line.
- It has its own catch branch:
- The program is still marked failed, so what is drawn does not change.
- It is counted in `st_left_out`, not `st_failed`, and does not touch
the 30-line allowance.
- One line the first time says the networks are left out and that this
is not an error. With `RX_VK_IR` set, each key is logged too.
- The "prepared" line names the count: `N failed, M networks left out`,
read through a new `shader_left_out()`.
- Real failures keep their text, their count and their 30 lines. Answers
`seed_compute` tries are not counted, as before.

## Testing

- New `tests/test_left_out_not_failed.py`: `translate_into`'s catch and
the left-out throw are cut from the source and run with a recording
`logf`. It runs 40 left-out networks, then one real failure, then three
more networks with `RX_VK_IR` set, then one network under a seed trial.
Expected and seen here:
  - the real failure's line is there;
  - failed 1, left out 43;
  - one line for the networks, and each key with `RX_VK_IR`.
- On main the same driver gives failed 44 and no line for the real
failure.
- The fixture also compiles with llvm-mingw for Windows.
- Linux renderer built (`hookshot_vk.so`).
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No AMD or Intel card ran it, and no game ran.

Ported to Longshot: Longshot's #177 had made these a trace line by their text (network_left_out); with this change they
are their own exception, counted apart and said once, so that branch and its constant go, and test_log_levels.py
checks the LeftOut path instead.

Co-authored-by: Cursor <[email protected]>
Since #1846 a DualShock 4's or DualSense's touchpad click opens and
closes the Insert menu as F1 does, and it can be nothing else:
`HOS_PAD_TOUCHPAD` was its own bit past `HOS_PAD_N`, so the Controls
page cannot take it and a listening binding masks it out. #2212 asks for
it as a remappable input (the map, Minus, is the natural place for it on
a PlayStation pad). #2979 may be the same request. Home, and Select with
Start, already open the menu on these pads.

## Change

- `gpu/vk/vk_api.h`: `HOS_PAD_TOUCHPAD` is the gamepad input after the
paddles, before `HOS_PAD_N`. The saved values of the other inputs do not
move. The executable and the renderer must agree on its bit, so
`MKWVK_API_VERSION` goes to 22, as it did when the paddles were added.
- `host/nn_hid.c` is unchanged: `pad_code`, `hos_bind` and
`bound_pad_buttons` take the touchpad as they take the paddles. The
options file keeps it, and the game gets the row it is bound to.
- `gpu/vk/vk_menu.cpp`:
  - `pad_name` calls it "Touchpad".
  - A listening binding no longer masks it.
- A click presses F1 only while no row of the Controls page has the
touchpad as Input 1 or Input 2. The new `touchpad_bound` asks the host's
bindings for every row that takes a gamepad input, the ocarina notes
included.
- Defaults do not change: unbound, and after Reset to defaults, a click
opens the menu as now.
- Prompts: the touchpad has no prompt artwork. Like the paddles, it
falls past `HOS_PAD_ART_N` and shows the bound Switch button's default
icon.
- `readme.md`: a bound touchpad does its binding instead of opening the
menu.

## Testing

- `tests/test_touchpad_menu.py`:
- The poll's button block still gives the touchpad its own bit, now the
input after the paddles.
- The menu's Controls table, `touchpad_bound`, `pad_name`, the F1 line
and the listening binding's mask are cut from `vk_menu.cpp` and run with
a stand-in host. Unbound, a click is F1. Bound to Minus's Input 2, or to
an ocarina note, it is not. A listening binding takes the click as its
input, and its name is "Touchpad".
- `tests/test_bound_keys.py`: an options file with the touchpad as
Minus's input reads back, and a click gives the game Minus through
`bound_pad_buttons`.
- Both fail on main and pass here. The new C++ fixture also compiles
with llvm-mingw for Windows.
- Linux renderer built (`hookshot_vk.so`).
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No DualShock 4 or DualSense ran it, and no game ran.
`texture_gpu_wrote` in `gpu/vk/vk_texture.cpp` notes each GPU buffer
write into guest memory as a record, start to end plus the submission
that wrote it. `gpu_wrote_into` finds the finished ones a texture was
not uploaded after. It starts at the one record before the texture's
address and walks forward, which is right only when records do not
overlap. The writer erased only the records a new write covered whole,
so a later, smaller write inside an older one left two records that
overlap.

For example, take `[0x100000, 0x140000)` at submission 10, then
`[0x110000, 0x120000)` at 11. For a texture at `0x130000`, the record
before it is the second one, which ends before the texture, and the
first is never looked at. The texture kept its old texels after the GPU
wrote them.

## Change

The writer keeps the records apart:
- A record the new write lands in keeps its parts before and after it,
each with its own submission. This applies to the one record that starts
before the write, and to one that starts inside it and runs past its
end.
- Then the new record is added.
- The "same start, covers it" shortcut stays.
- The cap of 65536 records is now enforced with a loop, because a write
inside a record adds two records.

`gpu_wrote_into` is unchanged.

## Testing

- New `tests/test_gpu_write_intervals.py`: both functions and the map
are cut from the source and run on such writes:
- The texture past the inner write finds 10, one inside it 11, one
before it 10.
- With only 10 finished, the outer ones are found and the inner one
waits.
- A write that runs past a record's end leaves the record's first part
with the old submission.
  - A write seen once is not found again, and a later one is.
  - After each write no two records overlap.
  - 100 interior writes into a full map keep it at 65536 records.
- On main the overlap cases fail. The fixture also compiles with
llvm-mingw for Windows.
- Linux renderer built (`hookshot_vk.so`).
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No game ran.
When `upload()` fails in `texture_touch` (`gpu/vk/vk_texture.cpp`), for
example for want of staging memory with the VRAM or RAM full, a BC3
texture is tried again 30 frames on (#1505). Every other kind was marked
uploaded with `upload_gen` moved on ("not retried every draw; a write to
its memory tries again"):
- A first upload that failed left an empty image, drawn as it was until
the game wrote that memory again, which for most textures is never.
- A re-upload that failed dropped what the CPU had written since the
last upload. It also dropped the GPU buffer write `gpu_wrote_into` had
just marked as seen.

This is the "textures degrade until a restart" kind of report on cards
with little memory (#993's family).

## Change

- One rule for every kind. `Texture::bc_retry_frame` becomes
`retry_frame`.
- On a failed upload:
  - `uploaded` and `upload_gen` stay as they were.
  - `gpu_wrote_seen` goes back to its value from before this touch.
  - `retry_frame` is set 30 frames on.
- A line is logged (40 at most). The BC3 line keeps its text, and the
other kinds get one like it.
- The wait moves up, right after the epoch check and before
`gpu_wrote_into` and the dirty scan. A waiting texture costs a compare
and loses nothing.
- A successful upload clears `retry_frame`.
- A texture evicted and made again over storage render targets took has
its `evicted_from_memory` entry taken before the upload. If the upload
fails, the entry is put back. Otherwise the retry would take the texture
for rendered storage and mark it uploaded, empty.
- An image no upload takes (no GPU address, multisampled, empty or too
big, packed depth-stencil) still fails at once, every time. It is still
marked uploaded, so it is not tried forever. Those checks move from the
top of `upload_inner` into `upload_never`, used by both.
- Nothing changes while uploads succeed.

## Testing

- New `tests/test_upload_retry.py` checks the source, because
`texture_touch` cannot run outside the renderer:
- The failed branch marks nothing done, sets the retry, puts
`gpu_wrote_seen` back and logs.
- The wait stands before `gpu_wrote_into` and the dirty scan, and
`gpu_wrote_seen`'s old value is taken before it.
  - A success clears the retry.
- `upload_never` is shared with `upload_inner` and still marks those
images uploaded.
  - A taken `evicted_from_memory` entry is put back on a failure.
- It fails on main.
- `tests/test_bc_upload_retry.py` follows the rename.
- Linux renderer built (`hookshot_vk.so`).
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- Not tested: a failed upload in the game. No card here runs out of
memory on demand, and no game ran.
- Not covered: for a texture in a virtual pool (GPU address at or above
1 << 40), the dirty scan has already taken a remap (`page_map`,
`map_sig`) when the upload fails. The retry uploads what the CPU wrote,
but not pages that were only remapped. That is the same as before;
replaying it would mean a whole re-upload of a sparse array while memory
is short.
## Why

Add the scarecrow dialogue as an adult to covered kinds so the ocarina
button re-mappings work during this dialogue. Notes fell back to default
ocarina controls per the safety net in #1930.

## What

One line: kind 11 (the scarecrow recalling/playing the recorded song at
him) joins the note-button kinds.
Developers only. `nx2/lift.py --write` run again over a gen folder,
after a saved boundary in chunks.txt moved to where the earlier lift had
cut a big chunk, kept that lift's split parts (`code_NNN_M.c`) beside
the new numbered chunk holding the same functions, and the link failed
on duplicate definitions.

`chunks.write` removed an earlier lift's stale numbered chunks and hot
pieces, but left its split parts for `split_big`. After the boundary
move the new chunk is the old first part byte for byte, so
`_cut_already` (meant for a worker that finished a cut whose result was
lost) found the old parts consistent with it and kept them as this
lift's. This needs the old first part to call none of the functions that
moved out: with such a call its declarations differ from the new
chunk's, and the old parts are rejected and removed, as before. The
packaged builder clears `code_*` before a translation and is not
affected.

## Change

`write()` removes an earlier lift's split parts together with its hot
pieces, before `split_big`, so `_cut_already` only sees parts made in
this lift. `split_big` is called only from `write()`, and nothing
between the removal and `split_big` reads the parts.

## Testing

- `tests/test_chunk_split.py`, new case: two lifts through
`chunks.write` with stand-in functions, the first with one chunk that
`split_big` cuts, the second with a saved boundary at the first part's
end. Each function is defined once and none of the first lift's parts is
left. On main it fails with 333 of the 400 functions defined twice.
- `run_all.py chunk hot spawn lift`: 11 passed.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
In Hookshot Builder, a game folder that was built from a supported
`.nup` can be rebuilt from an extracted dump. When that dump is of
another game version, the build is refused ("This is build X. Hookshot
supports build Y"), which is right, but `nx2/make_game.py` checked the
version only after the dump's modules had replaced the folder's saved
copy (`.hookshot_exefs`). The stamp of the earlier `.nup` stayed.
Choosing that `.nup` again then said "reading it again is skipped",
built from the refused modules and was refused too, until the folder was
deleted.

## Change

In the extracted-dump branch of `main()`:

- `check_supported_build` runs right after the dump's modules are
unpacked, before the romfs is linked in and before `.hookshot_exefs` is
touched. A refused dump leaves the folder as it was.
- An accepted dump removes the `.nup` stamp (`.hookshot_romfs`), since
the saved copies are no longer that `.nup`'s. The next build from the
`.nup` reads it again.

The `.nup` branch already checks the build before it replaces anything;
it is unchanged.

## Testing

- `tests/test_make_game_cache.py`, two new cases (the cache test's fake
`.nup` and modules, no game):
- A refused extracted dump: the folder's files are byte for byte as
before, and the earlier `.nup` then reuses its copy and reaches the lift
with the supported build.
- An accepted extracted dump: its modules are saved, the `.nup` stamp is
gone, and the earlier `.nup` is read again.
  - Both fail on main and pass here.
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No real game build was run.
**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
(#2345), 52 (#2669, #2604, #2567), 54 (#2567, #2530), 60 (#2261) and 64
(#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]>
With HDR on **Native**, the temporal chain gives DLSS (#2243) and FSR 4
(#2342) the game's colour as linear light, past 1.
FSR 3.1 on the chain (#2305) still got it clamped at 1, so with FSR 3.1
every highlight stopped at the paper white. This
gives FSR 3.1 the same HDR colour. Its passes are already built for HDR
colour (`FFX_FSR3UPSCALER_OPTION_HDR_COLOR_INPUT`)
and work out their own exposure, so no pass or shader changes: only the
chain engine says it takes HDR.

**What changed**
- `vk_fsr3.cpp`, the chain engine:
- `chain_prepare` decides HDR each frame from `present_native_pq`, as
DLSS and FSR 4 do: what the game presents, not
the option. `RX_VK_FSR3_HDR=0` keeps the clamped colour, for
comparisons.
- `chain_hdr` tells the chain (`TemporalEngine::hdr`), so the chain's
existing HDR prep/out pair runs for it: linear
    light in (`kSquare`, ceiling 16384), its picture taken back linear.
- When that changes, the next evaluation starts a new history (`reset`),
since the history kept was in the other
encoding. DLSS and FSR 4 get the same by making their feature or context
again.
- The log says `vk: fsr3 on the chain: HDR colour, linear (the game's
native HDR)` when it turns on.
- Docs: the readme's FSR 3.1 section, `docs/development.md`
(`RX_VK_FSR3_HDR=0`), and a row in `docs/tested-configurations.md`.
- Tests: `test_hdr_encode.py` gets `check_fsr3`, beside `check_dlss` and
`check_fsr4`: HDR decided from
`present_native_pq` (not `opts.hdr`), the switch, the history reset, the
engine's `.hdr`, and the passes built for HDR
  colour. Removing the reset or the engine's `.hdr` each fails it.

**Not changed:**
- SDR, and HDR with the SDR picture expanded: `chain_hdr` is false
there, so the chain runs the same LDR passes as on main.
- FSR 3.1 with its own inputs (`RX_VK_FSR3_CHAIN=0`) stays LDR.

**Testing**
- `python -I -B tests/run_all.py`: 449 passed, 2 failed, 21 skipped
(Linux). The two failures fail on main too:
`test_env_on` (`HOS_FPS_LOG` in `host/oot_high_fps.c`) and
`test_linux_saves_move`.
- In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel
7.3.3), KDE Plasma Wayland, windowed, 2560x1440 HDR10
(HDR Native, paper white 110 nits = PQ code 132, peak 400). Host
v2.1.0-main-local; renderer main ef81aee7 plus this
  branch. "Off" is `RX_VK_FSR3_HDR=0`, which is what main does.
- Every HDR run logged `vk: fsr3 on the chain: HDR colour, linear (the
game's native HDR)` once, then drew FSR 3.1 on
every frame (600 of 600 per log line), with no device lost and no
validation messages. The only fallback to the
network was one frame at the teleport (`no scene camera this frame`), in
every FSR 3.1 run, with or without this.
- Castle Town market at noon, the still frames' PQ codes (0-255 of the
HDR10 window's 8-bit shot):

| Upscaler | Brightest pixel | 99.9th percentile | Pixels above the
paper white, per million |
    |---|---|---|---|
| FSR 3.1 100%, this change (2 runs) | 163, 162 | 139 | 16745, 16510 |
    | FSR 3.1 100%, off (2 runs) | 147, 147 | 138 | 14465, 15115 |
    | FSR 3.1 Quality (67%), this change | 160 | 138 | 14344 |
    | FSR 3.1 Quality (67%), off | 147 | 138 | 13857 |
    | The game's own upscaler, 100% (reference) | 182 | 139 | 17578 |
    | FSR 4 Native AA, HDR (#2342) | 156 | 138 | 13276 |

Off, every run stops at the same code, 147. With the change, the sunlit
highlights go past it, towards the game's
own. The rest of the picture is the same: mean code 63.5 in both, and no
colour shift side by side.
- Kokiri Forest showed no difference: nearly all the pixels there above
the paper white are Navi's, as bright with the
    change as without it.
- The Quality and off rows were measured on an earlier revision of this
branch that also made FSR 3.1's luma history
RGBA16F (see below); with the history as it is here, 100% gave the same
highlights (163, 162 against 163, 162).

**Left out, a possible follow-up:** FSR 3.1's luma history
(`rw_luma_history`, the luma instability pass) is RGBA8,
because AMD's GLSL callbacks declare it `rgba8`, so it holds luma up to
1 only. I tried an RGBA16F history (a patched copy
of the callbacks header for `scripts/fsr3_shaders.py`). It made no
measurable difference to highlights or flicker in HDR,
nor to flicker in SDR (5 alternated pairs, within run-to-run noise), so
it isn't in this PR. If someone sees highlights
flicker with FSR 3.1 in HDR, that's where to look.

**Not tested:**
- other GPUs and drivers;
- Windows;
- Steam Deck;
- switching HDR on and off while FSR 3.1 is drawing (the history reset
is code-reviewed, not exercised in game).

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
**The bug (#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 #2579

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
With Ultrawide menus on either Centred setting and a picture wider than
16:9, entering a new area flashes that area on both sides of the screen,
overbright at first, beside a black middle (#2220).

## Why

The loading cover between two areas (`UILoadingFadeScreen`) is one of
the screens the setting keeps in the middle 16:9, and the game draws the
new area under the cover before it opens. On a build without the change
(main ef81aee7 with #2545) two entries had 12 and 10 presents with the
middle black and the sides at 194 to 244 of 255. The sides showed those
frames.

EjianYT found the cause in #2220 and showed that leaving the cover at
the picture's full width (`kMenus` 0xa -> 0x2) hides it. That stretches
the cover's loading icon across the wide picture with it, so this change
leaves the cover where it is and closes the sides for as long as it
shows.

## The change

- `host/hos_main.c`: `hos_wide_cover()` reads how far the cover is
closed, 0 to 1, from the cover screen's layout: the opacity of its black
picture's pane times those of the two panes above it.
`nx2/oot_wide.py`'s HUD site notes the screen's address as its
projection is set up. Each link is checked before it is used (the
screen's vtable, the pane's own layout pointer, its 3840 x 2160 size,
its parents in the tree, each opacity within 0 to 1, no address in the
first MB, which is never mapped); anything else reads as 0, the sides as
they were. The offsets are this game version's.
- `MkwVkHost.wide_cover`, API version 22.
- The renderer asks as the game presents each frame, on that thread, and
carries the level with the frame. The window's scale pass
(`fg_scale.comp`) darkens the picture outside the middle by it. A pixel
is darkened as it is read, before the filter and the sharpen, so the
edge stays where the game's cover ends at any window size and no side
light is filtered into the middle. The blurred menu sides are darkened
by the same level.
- The game blends its cover over the picture's light, not over its codes
(measured on shots of an opening cover: a pixel's decoded light is the
same fraction of its final light at every brightness, its code is not).
So the sides' light is scaled: the code to light, times 1 - level, back
to the code, through sRGB, or SMPTE ST 2084 when the game presents its
own HDR10 picture. Scaling the codes had the sides open darker than the
middle.
- The game may have moved the cover on for its next frame by the time it
presents one: with every present shot and the level logged, the reading
ran one present ahead of the picture through one area's entry and by
none or one through another's. A frame's sides take the larger of its
reading and the one before, so they are never more open than the middle
and at most one frame behind it.
- With frame generation the sides are closed in its copy of the game's
frame (`fg_copy.comp`, 12 more bytes of push constants) instead, before
the frames between are made, so the renderer's interpolation, FSR 3.1's
and DLSS Frame Generation treat the sides as they do the middle; the
display thread's scale pass then leaves them alone.
- 16:9 and narrower, Ultrawide menus on Full width, and no cover
showing: the level is 0 and nothing changes (the scale pass is skipped
as before when it has nothing else to do).

## Tested

Windows 11, RTX 4070 Ti SUPER, NVIDIA 621.24, no window
(`HOS_HEADLESS=1`), the picture in 3440x1440's shape
(`HOS_ASPECT=2.3889`), Ultrawide menus on Centred, blurred sides. Four
runs of one scripted route with two area entries (teleports), every
present around them saved as a picture and read back in linear light.
The first three were on this branch's commit before its rebase (on
0ec31ff6, API version 20); the rebase onto 9b6072cd had conflicts only
where main added to the same lists (`vk_api.h`, `vk_framegen.h`,
`rx_backend.c`, one test). The fourth is the rebased commit.

- The change's own build: while the cover is closed the sides are black,
mean light 0 in all 181 and 135 presents of the two entries (before: the
12 and 10 lit presents above).
- While the cover opens (33 and 22 presents), a strip just outside the
middle 16:9's edge against the strip just inside it, each as a fraction
of its own light after the cover: the outside one is behind in every one
of them, by 0.04 to 0.19.
- A build that also logs the level it uses, per present: the outside
strip follows 1 - level and is under the inside one on every present the
cover shows in both openings (24 and 29 presents).
- Frame generation 2x (the renderer's own), game frames and made frames
saved: sides black while the cover is closed (125 and 116 game
presents); 42 made frames around the two openings lie between their two
game frames in the sides (2 in the middle are 0.05 under both, after the
cover has gone).
- The rebased commit, one run: the sides are black, mean light 0, in all
190 and 141 presents with the cover closed. While it opens (23 and 35
presents) the outside strip is behind the inside one in 22 and 35 of
them, by up to 0.09 and 0.26; in the first opening's last present it is
0.03 ahead (0.985 against 0.959 of the light after the cover).
- `python -I -B tests/run_all.py` on the rebased commit, Linux: 473
passed, 51 skipped, 4 failed. `test_bake_portability` (no `zstandard`
here), `nx2/test_mesh_first_types` and `nx2/test_texture_init_barrier`
fail the same way without the change; `test_recorder_sync` failed in the
full run and passes alone. New: `tests/test_wide_cover.py`,
`tests/test_wide_cover_light.py`, and cases in
`upscale_independent_test.cpp` and `present_source_crop_test.cpp`.

**Not tested:** a real ultrawide window (the runs had no window), HDR
output (the ST 2084 path is covered by `test_wide_cover_light.py` only),
FSR 3.1 and DLSS Frame Generation, Linux in the game, AMD and Intel
cards, other game versions.

## Other work on #2220

#2851 (draft) keeps the cover at the full width through the keep mask,
and its review asks for a per-pane version on top of #2843 instead,
which would widen only the cover's black pane. This change needs
neither: it works on main as it is and leaves the layout alone. If the
pane version lands first I am glad to close this, or to cut it down to
whatever is still useful beside it.

Fixes #2220
## 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]>
### Problem

On Linux, `hos_reserved_cpus()` in `host/hos_thread.c` selects the last
performance core (updated for Intel hybrid CPUs in #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
#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.

---

### 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**
#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 (#1634, #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 #1830: omgitsgela's Steam Deck run there is the
before/after. If they're OK with it, their pair would go well in #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]>
Fixes #2182 (one of the reports collected in #2503).

## What was wrong

With Audio output on **Auto**, Hookshot told the game 5.1 whenever the
default output device had 6 or more *channels*. An audio interface
reports all of its outputs as channels without naming them as speakers.
A Focusrite Scarlett 18i8, for example, has 8 channels and a channel
mask of `0`. The game then mixed for 5.1, and everything it puts in the
centre or behind the player went to outputs with nothing connected. That
includes voices (Saria in the ocarina cutscene), the ocarina and Link's
own sounds. Those sounds were silent while the rest of the game played
normally.

The readme already says Auto follows the device's *speakers*. The code
counted channels.

## The change

- `host/nn_audio.c`: `au_device_channels` now reads the mix format's
channel mask through a new `au_layout_channels`. A device counts as 5.1
only when its mask names a centre and a pair of speakers behind or
beside the player (5.1 back, 5.1 side, 7.1). Everything else is stereo:
mask `0`, no mask, quad, or 5.1 without a centre. The log says why, for
example: `audio: the default output has 8 channels but not 5.1 speakers
(channel mask 0): counted as stereo`. As stereo, the game's own downmix
keeps every sound.
- The Stereo and 5.1 menu choices and `HOS_AUDIO_CHANNELS` work as
before. Real 5.1 and 7.1 systems still get 5.1.
- Linux is unchanged: SDL2 reports only a channel count, not speaker
positions.
- `readme.md`: one sentence on what Auto counts.

## Testing

- New game-free check, `tests/test_audio_speaker_layout.py`. It compiles
the real `au_layout_channels` and tests it against 13 mix formats
(stereo, mono, 5.1 back and side, 7.1, 7.1 wide, interfaces with mask 0,
quad, 5.1 without a centre, formats with no mask). It also checks that
`au_device_channels` goes through the new function. It passes locally
with MSVC; `run_all.py` passes `{cxx}` on CI.
- The changed `au_device_channels` was compiled on its own and run on
the reporting PC (Windows 11, Scarlett 18i8 as the default output, RTX
3080). It reads the live mix format (8 channels, mask `0`) and returns
2.
- Not tested yet: an in-game run on v2.1.0 with this build. Before this
change, choosing **Stereo** in the menu works around the problem.

Co-authored-by: Claude Opus 5.5 <[email protected]>
**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 (#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]>
**Why.** On 6 and 8 GB cards, VRAM fills over play time and the frame
rate fades (#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]>
**The problem.** The temporal chain (`gpu/vk/vk_temporal.cpp`) starts a
new history only when the camera is lost, the size or target changes, or
a frame was skipped. An area load, a spawn or a cutscene cut moves the
camera 5 to 180 m in one frame. DLSS and FSR then blend the new view
with the old one (smears and ghosting right after loads), and their
frame generation makes frames between two unrelated pictures.

**The fix**
- `dlss_math::camera_pose` reads the camera's position and look
direction straight from the view-projection: the point where clip x, y
and w are all 0, and the w row. A jitter doesn't move either.
- `dlss_math::camera_cut` treats either of these as a cut, per 60 Hz
frame, scaled by the time between frames (¼ to 4 frames):
  - a move over 5 m, or
  - a turn over 30° together with a move over 0.5 m.
- Normal play (~0.05 m and 0.08° a frame) and a quick camera swing round
Link (11–18° and ~1 m a frame) don't count. A lower turn threshold
restarted the history on every frame of a swing and showed the blocky
render size.
- A cut sets the frame's `reset`, which already reaches DLSS, FSR 3.1/4
and their frame generation.
- Each cut is logged, at most 24. `RX_VK_TEMPORAL_CUT=0` turns the test
off.

**Tests**
- `tests/test_camera_cut.py` builds real look-at × perspective matrices.
It recovers the camera exactly (also jittered) and checks play, a swing,
a 35° turn with a move, an area load, the time scaling, an orthographic
matrix and the first frame.
- `test_temporal_history` (fixture updated), `test_dlss_math`,
`test_dlss_checks`, `test_dlss_native_transition`,
`test_temporal_jitter` and `test_upscaler_rows` pass.
- `vk_temporal.cpp` compiles.

**Not yet verified in-game:** teleporting with DLSS or FSR on (the log
line plus no ghosting after an area load).

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
When the Linux renderer fails to load, the log says only `(2)`. That
covers a missing file and a `hookshot_vk.so` from another build alike,
though the second is the one that needs explaining: it takes symbols
from the executable, so it fails at `dlopen`, before `mkwvk_init`'s
version check, and the game quietly falls back to WebGPU.

## Change

- **`host/posix/win32_posix.c`:** `LoadLibraryA` keeps `dlerror()`'s
text for the last failure, per thread, and `hos_load_error()` returns it
(`""` after a success). `GetLastError()` is still
`ERROR_FILE_NOT_FOUND`, as before.
- **`host/rx_backend.c`:** `load_vulkan`'s failure line prints that text
on Linux:

  ```
rx: RX_BACKEND=vulkan, but .../hookshot_vk.so did not load
(.../hookshot_vk.so: undefined symbol: mkwvk_...): WebGPU
  ```

Windows keeps the error number, which already tells a missing file (126)
from a missing function (127).

Part of the dev-loop proposal's B1 (#2636): a renderer swap during
development needs this line to say why the new one was refused.

## Testing

- **`tests/test_renderer_load_error.py` (new):**
- everywhere: the source keeps the text and logs it (it fails on
`main`);
- on Linux: builds `win32_posix.c` as it is and loads, through
`LoadLibraryA`, a `.dll` name with no library behind it, a library that
needs a symbol nothing has, then a good one.
- **The Linux part, run by hand on WSL Ubuntu (gcc), as the check does
it:**

  ```
failed|2|<tmp>/absent_vk.so: cannot open shared object file: No such
file or directory
  failed|2|<tmp>/needs.so: undefined symbol: mkwvk_from_another_build
  loaded|0|
  ```

- **Every check that reads the changed files (37), through `run_all.py`,
rebased on today's `main`, on Windows:** 34 pass;
`nx2/test_bc_storage_views.py`, `nx2/test_dll.py` and `nx2/test_leaf.py`
fail the same way on `main` here. The new check's Linux part is skipped
on Windows.
- **Not run:** the game on Linux. The change is the failure path's log
line.

See #2636 (B1).

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

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: NotRexed <[email protected]>
**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]>
On Intel GPUs, Kokiri Forest shows big white glowing blocks and haze
over the trees and houses, and save thumbnails come out white. The cause
is in how a long submission is cut into parts: each part's end wrote the
stream's uniform updates into guest memory halfway through the
submission, so the draws of the later parts read a state an unsplit
submission never shows.

Part of #999. The mechanism was found by ploob-boop on an Arc 140V
(their comment in #999: a whole-screen white flash every 8 frames); this
PR checks it on a Core Ultra 7 155H and against current main.

## What changed

- `maybe_split` (`gpu/vk/vk_replay.cpp`) no longer calls
`flush_ub_updates` at a part's end, nor moves `ub_first_of_recording`.
The submission's last part writes all its updates, exactly as an unsplit
submission does in `finish_recording` (`RX_VK_CHUNK=0` behaves that way
already).
- Measured on Intel only, so it is the default on Intel only (`vendorID
== 0x8086`). `RX_VK_SPLIT_UBFLUSH=1` keeps the old behaviour; `=0` turns
the fix on for any GPU, for anyone who wants to try it on AMD or NVIDIA.
- Other GPUs run exactly the code they ran before.

## How it was tested

Core Ultra 7 155H (Arc Graphics, Meteor Lake, 8 Xe-cores), driver
32.0.101.9175, Windows 11, 32 GB. Settings: output 1920x1080, Render
resolution 4, handheld profile. Same save in Kokiri Forest (outside
Link's house), scripted start, camera still, 300 s per run, a screenshot
every 60 frames, the game closed normally.

Same binary for both arms: main 9b6072c plus this PR (the same patch as
on current main), #2999 and a local change that lets Intel compute
stages go of the 32-lane pin as #2564 does; the "old" arm is
`RX_VK_SPLIT_UBFLUSH=1 RX_VK_PIN_NEEDED=0` (pin as main). Runs
alternated old/new.

| | white-block frames in play (of ~25-40 captured) | fps (median) |
|---|---|---|
| old 1 / new 1 | 0 / 0 | 5.4 / 5.7 |
| old 2 / new 2 | 9 / lost the device at 80 s* | 5.5 / - |
| old 4 / new 4 | 7 / 0 (lost the device at 176 s*) | 4.7 / 5.9 |
| old 5 / new 5 | 11 / 0 | 5.7 / 6.0 |

An earlier series with this change alone on the same build: 7, 1 and 1
white-block frames in the three runs without it, 0, 0 and 0 with it, no
fps difference.

\* The device losses are main's known ones (#999), in compute programs
ab7699ee7f536755, aba5e6af8d4683eb and 499dbfd0721d2858; plain main
9b6072c loses the device in the same programs. Counting both series, the
runs without this change lost it in 2 of 8 and the runs with it in 5 of
8. I do not think this change causes them (it only moves when uniform
data is written, and the programs lost are compute ones that read none
of it), but at this number of runs I cannot rule it out either; more
runs on other Intel GPUs are welcome.

Screenshots (same frame of the same scripted run, left without the fix,
right with it):

<img width="1280" height="720" alt="kokiri_before_split_flush"
src="https://github.com/user-attachments/assets/0221080a-cba9-473b-8722-2989133c46b5"
/>
<img width="1280" height="720" alt="kokiri_after_split_flush"
src="https://github.com/user-attachments/assets/7101303a-f798-4f77-8602-a49e9cb485ed"
/>
`python -I -B tests/run_all.py`: the checks that read `maybe_split` pass
(`test_parallel_small_cb`, `test_readback_part`,
`test_value_rec_last_part`); the 10 failures this Windows PC also shows
on plain main (builder, updater, portable archive) are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: NotRexed <[email protected]>
With "Controller in background" on (or `HOS_INPUT_BACKGROUND=1`),
`read_input`'s branch for a window behind another merged the pads
straight into the game's input. The Insert menu's hold, which the branch
for the window in front applies (the pads are the menu's while it is
open, and after it closed until they are let go), was missing there. A
button pressed with the menu open behind another window, or still held
after it closed, reached the game.

## Change

The hold, the read and the merge are one helper, `pads_merge`, used by
both branches. The pads are still read while held back, so the letting
go is seen. `player_used()` is still called only in front, as before,
and the keyboard and mouse stay out behind other windows.

## Testing

- `tests/test_input_background.py` and its fixture: a new case behind
another window with the pads allowed there. With the menu open, B and
the sticks do not reach the game, though the pads are read; with the
menu closed and B still held, nothing; after B is let go and pressed
again, B and the sticks arrive. On main the first frame already carries
B. The other 17 cases pass as before.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- Not tested in the game.

Co-authored-by: NotRexed <[email protected]>
## What this fixes

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.

## Why merge

- 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.

## Change

- `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.

## Tested

- `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.

## Not covered

- 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]>
## 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 #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 #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]>
## 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 #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 #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 #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 #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]>
## 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 #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 #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]>
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 #2602).

These changes were measured only together with #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 + #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 + #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 + #2709 | 40.7 (−1.6 %) | 80.91 | 32.1 | 117.5 | 1 / 0 |

- The 6-thread cell was measured with #2709's 5 µs spin, which #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 #2602's fix of a main regression,
41 → 53 fps on this test) are replaced by these.

## Merge order with #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:
- #2765's Broadcast reserves the mutex for `cv_in - cv_owed`, the
waiters under the condition variable's lock, instead of `cv_waiting`.
- #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]>
## 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 #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 #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, PR #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 #2484

#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: #993 (where #2180 was consolidated).
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
#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]>
The short stall when something first comes into view (#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 #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.
Split out of #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]>
**Why.** #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 #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 #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 #2826 does (the excess and `kExcessSlack`); a larger one is
cut to 7/8 of the rule's budget, as before #2826. The budget's size is
unchanged.
- With issue #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 #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 #2826's gain, so the bound stayed a
sixteenth of the budget (176 MB for this card's Market, 227 MB for issue
#2780's 3,639 MB).

**3. The Lake with issue #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 #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 #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]>
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.

## 1. Keep the staging arenas, and make them before the game's memory

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.

## 2. Answer repeated texture views from a direct-mapped front cache

`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.

## Also

- `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.

## Testing

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]>
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!
### The bug

`upload_inner` (gpu/vk/vk_texture.cpp) uploads a texture in runs of
rows. For a sparse texture, each run first queues the binds of the tiles
it touches with `texture_resident_queue`: the tiles are marked bound at
once, and the binds go into `sparse_binds`, one list shared by every
texture. `texture_resident_flush` later binds the whole list with
`vkQueueBindSparse`, into one image only: `VkSparseImageMemoryBindInfo`
takes a single `image` for the list.

Each run takes its staging memory with `staging()`. When that failed
part-way, the code was:

```cpp
if (!st.buffer) return false;
```

so it returned with the binds of the earlier rows still in
`sparse_binds`. Step by step:

1. Texture A (sparse) uploads. Rows 0..N queue the binds of their tiles;
those tiles are marked bound.
2. The staging for row N+1 fails, and `upload_inner` returns false. A's
binds are still in `sparse_binds` and were never made.
3. Texture B (sparse) uploads. Its `texture_resident_flush` binds the
whole list into **B's** image, including A's binds. The memory meant for
A's tiles goes to the same coordinates of B's image.
4. A's tiles stay marked bound, so no later write queues them again. A's
later copies into those tiles land in unbacked memory, and B gets memory
it did not ask for.

The other exits of that loop (a new staging arena, the end of the
upload) already call `texture_resident_flush(&tex)` first. Only the
staging-failure exit did not.

### What changed

Only that exit, in `upload_inner`:

```cpp
if (!st.buffer) {
	texture_resident_flush(&tex);
	return false;
}
```

The binds the earlier rows queued are now made into A's own image before
giving up, as the other exits do. The success path is unchanged.

A texture is only sparse when it is a 2D array over `RX_VK_TEX_MAX_MB`
(1.5 GB) on a device with sparse support, which is MKW's course-map
case.

### Test

`tests/test_sparse_staging_failure.py` is a source check on
`upload_inner`. In the part of the loop after the first `staging()` call
and before `texture_resident_queue`, every `return false;` must have a
`texture_resident_flush(&tex);` in the same block. The old `if
(!st.buffer) return false;` must be gone. On main it fails with "a
staging failure returns without binding the sparse blocks queued before
it"; with the fix it passes.

### How tested

- The new test fails on main and passes with the fix.
- The tests that reference vk_texture.cpp pass.
- Release build OK.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin), nightly with this
change, about 40 minutes, log clean. This checks that non-sparse uploads
are unaffected; the sparse path itself is not reached there.

Assisted-by: GitHub Copilot
### The bug

Read-backs (texture write-backs to guest memory) use their own command
pool `rb_pool`, command buffer `rb_cmd` and fence `rb_fence`
(gpu/vk/vk_texture.cpp):

- `readback_begin` resets the pool and begins `rb_cmd`.
- `readback_submit_wait` resets `rb_fence`, submits `rb_cmd` with it,
and waits up to 5 s.

When that wait timed out, the function returned false, but nothing
remembered that the submission was still running. Step by step:

1. A read-back is submitted. The GPU is slow (heavy load, a long batch
before it), and `vkWaitForFences` returns `VK_TIMEOUT` after 5 s.
`rb_cmd` is still in the pending state and `rb_fence` is still tied to
it.
2. The next read-back calls `readback_begin`:
- `vkResetCommandPool(rb_pool)` while `rb_cmd` is pending. This violates
**VUID-vkResetCommandPool-commandPool-00040**: "All VkCommandBuffer
objects allocated from commandPool must not be in the pending state".
- `vkBeginCommandBuffer(rb_cmd)` while it is pending. This violates
**VUID-vkBeginCommandBuffer-commandBuffer-00049**: "commandBuffer must
not be in the pending state".
3. `readback_submit_wait` then calls `vkResetFences(rb_fence)` while it
is still tied to the unfinished submission. This violates
**VUID-vkResetFences-pFences-01123**: "Each element of pFences must not
be currently associated with any queue command that has not yet
completed execution on that queue".

This is undefined behaviour: the GPU can be running commands that the
CPU is rewriting.

There was a second problem in the creation. `readback_begin` makes the
pool, then the command buffer, then the fence, all only when `rb_pool`
is null. If `vkAllocateCommandBuffers` or `vkCreateFence` failed,
`rb_pool` stayed set with `rb_cmd` or `rb_fence` null. Every later call
then skipped the creation and used a null command buffer or fence.

### What changed

- New `bool rb_pending`. `readback_submit_wait` sets it when its wait
does not succeed:

  ```cpp
rb_pending = vkWaitForFences(dev.device, 1, &rb_fence, VK_TRUE,
5000000000ull) != VK_SUCCESS;
  return !rb_pending;
  ```

- `readback_begin` first waits for a pending submission, before any
reset. If it is still not done, this read-back is skipped (returns
false), as a failed read-back already was:

  ```cpp
  if (rb_pending) {
if (vkWaitForFences(dev.device, 1, &rb_fence, VK_TRUE, 5000000000ull) !=
VK_SUCCESS) return false;
	  rb_pending = false;
  }
  ```

- If the command buffer or the fence cannot be made, the pool is
destroyed and all three handles go back to null, so the next call makes
them again instead of using them half made.

When no wait times out, the behaviour is the same as before.

### Test

`tests/test_readback_pending.py` is a source check on `readback_begin`
and `readback_submit_wait`. It checks that:

- `rb_pending` exists.
- In `readback_begin`, `if (rb_pending)` and its `vkWaitForFences` on
`rb_fence` come **before** `vkResetCommandPool`.
- The failure branch of the creation destroys `rb_pool` and sets it back
to null.
- `readback_submit_wait` stores the result of its wait in `rb_pending`.

On main it fails ("a timed-out read-back is not remembered"); with the
fix it passes.

### How tested

- The new test fails on main and passes with the fix.
- The tests that reference vk_texture.cpp pass.
- Release build OK.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin), nightly with this
change, about 40 minutes, log clean. Read-backs run normally there, but
none timed out, so the timeout path itself was not reached.

Assisted-by: GitHub Copilot
### The bug

`copy_t2t` (gpu/vk/vk_xfer.cpp) makes a texture-to-texture copy in one
of three ways:

1. `vkCmdResolveImage`: a multisampled source into a single-sampled
destination of the same size.
2. `vkCmdCopyImage`: same size, same block size, same sample count.
3. `vkCmdBlitImage`: everything else, which means scaled or converting
copies.

Branch 3 called `vkCmdBlitImage` without checking any of the rules the
spec puts on it. Step by step, with one example:

1. The game copies a texture into another of a different size or a
different format, for example an unsigned-integer texture into a
normalized-colour one.
2. Neither the resolve nor the copy branch applies, so the copy goes to
`vkCmdBlitImage`.
Linear is used when the game asks for it, or when the destination keeps
its level at a smaller size than the source (`d.down > s.down`).
4. When one of those rules is broken, the command is invalid usage and
the result is undefined. Some drivers tolerate it; others may write
garbage or lose the device.

The rules, with their exact text from the spec (`validusage.json`):

| VUID | Text |
|---|---|
| VUID-vkCmdBlitImage-srcImage-01999 | The format features of srcImage
must contain VK_FORMAT_FEATURE_BLIT_SRC_BIT |
| VUID-vkCmdBlitImage-dstImage-02000 | The format features of dstImage
must contain VK_FORMAT_FEATURE_BLIT_DST_BIT |
| VUID-vkCmdBlitImage-filter-02001 | If filter is VK_FILTER_LINEAR, then
the format features of srcImage must contain
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT |
| VUID-vkCmdBlitImage-srcImage-00229 | If either of srcImage or dstImage
was created with a signed integer VkFormat, the other must also have
been created with a signed integer VkFormat |
| VUID-vkCmdBlitImage-srcImage-00230 | If either of srcImage or dstImage
was created with an unsigned integer color VkFormat, the other must also
have been created with an unsigned integer color VkFormat |
| VUID-vkCmdBlitImage-srcImage-00231 | If either of srcImage or dstImage
was created with a depth/stencil format and the maintenance12 feature is
not enabled the other must have exactly the same format |
| VUID-vkCmdBlitImage-srcImage-00233 | srcImage must have been created
with a samples value of VK_SAMPLE_COUNT_1_BIT |
| VUID-vkCmdBlitImage-dstImage-00234 | dstImage must have been created
with a samples value of VK_SAMPLE_COUNT_1_BIT |

Hookshot does not enable maintenance12, so -00231 applies as written.
The images are created with `VK_IMAGE_TILING_OPTIMAL`, so the format
features that matter are `optimalTilingFeatures`.

### What changed (gpu/vk/vk_xfer.cpp)

Before, branch 3 went straight to the blit:

```cpp
} else {   // scaled or converting
	VkImageBlit bl{};
	...
	const bool linear = ((p.flags & NVN_COPY_FLAGS_LINEAR_FILTER_BIT) || d.down > s.down) && color;
	vkCmdBlitImage(..., linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
}
```

Now it first checks the rules above, and skips a blit that breaks any of
them:

```cpp
if (!(tiling_features(s.tex->format) & VK_FORMAT_FEATURE_BLIT_SRC_BIT) ||        // -01999
	!(tiling_features(d.tex->format) & VK_FORMAT_FEATURE_BLIT_DST_BIT) ||        // -02000
	format_class(s.tex->format) != format_class(d.tex->format) ||                // -00229, -00230
	(!color && s.tex->format != d.tex->format) ||                                // -00231
	s.tex->samples != VK_SAMPLE_COUNT_1_BIT || d.tex->samples != VK_SAMPLE_COUNT_1_BIT) {   // -00233, -00234
	warn(13, "texture blit the device cannot make: skipped");
	full_barrier(cmd);
	return;
}
```

Linear filtering is only used when the source format supports it
(-02001), and only for float or normalized formats:

```cpp
const bool linear = ((p.flags & NVN_COPY_FLAGS_LINEAR_FILTER_BIT) || d.down > s.down) && color &&
					(tiling_features(s.tex->format) & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) &&
					format_class(s.tex->format) == 0;
```

Two new helpers in the same file:

- `format_class(VkFormat)` returns:
  - 1 for the unsigned-integer formats,
  - 2 for the signed-integer formats,
  - 0 for everything else (float, normalized, sRGB).
- `tiling_features(VkFormat)` returns the format's
`optimalTilingFeatures`:
- For the core formats (values below 256), it asks the driver once per
format and keeps the answer in a lock-free `std::atomic<uint64_t>`
table. Bit 63 marks a format as already asked.
  - Formats from extensions (256 and above) are asked each time.
- A scaled copy checks both sides, so the cache avoids querying the
driver twice per copy on the replay thread.

The depth/stencil condition is already caught earlier in `copy_t2t`, by
warn 7 ("texture copy between unlike depth formats: skipped"), so it
never fires here. It is kept in the check so the blit's own rules stay
in one place.

`warn` logs each key at most 3 times.

### Behaviour

- A blit that follows the rules goes through exactly as before.
- A blit that breaks a rule is skipped, and its destination is left as
it was. Before, it was undefined: it may have looked right on some
drivers, for example an integer/float mix or a multisampled image on a
driver that tolerates it. Now the destination is stale instead.
- This matters most on devices with a narrower format list (mobile and
integrated GPUs, layered drivers such as MoltenVK), and for games or
scenes that blit less common formats.

### Changes after review

- The format features come from the `tiling_features` cache, so they are
not queried twice per scaled copy.
- The first push of that change left one use of the old local variable
in the linear-filter check. The vk_xfer tests don't compile that code;
the full release build caught it, and it is fixed.

### Test

There is no dedicated unit test for these rules. The change is reviewed
against the VUIDs above. The existing checks that compile or read
vk_xfer.cpp pass (6, including `test_vulkan_copy_span_gpu_compile`).

### How tested

- Release build (`nx2/build.py --standalone`, Windows, llvm-mingw).
- The 6 vk_xfer.cpp checks pass.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin):
  - Two full sessions on build g54d01844.
- One session of about 40 minutes on nightly g57471723, which includes
this change.
  - No visible regression, no device loss.
- "texture blit the device cannot make" does not appear in any of those
logs. Every blit OoT makes on this device follows the rules, so the skip
path is not reached there. This change guards against invalid usage; it
does not fix a visible glitch in OoT.

Assisted-by: GitHub Copilot
### The bug

ASTC textures are kept in one of three ways, depending on the GPU:

- **Decoded to RGBA8** on most GPUs.
- **Re-encoded as BC3** for some big textures.
- **Kept as ASTC blocks** when the device samples ASTC natively
(`textureCompressionASTC_LDR`, `astc_native()`). The image then has an
ASTC `VkFormat`.

`clear_tex` (gpu/vk/vk_xfer.cpp) clears a texture in one of two ways:

1. **Whole level**: when the clear covers the whole level, the image is
not 3D, and the format is not BC (`raw`), it uses `vkCmdClearColorImage`
for colour, or `vkCmdClearDepthStencilImage`.
2. **Region**: otherwise, a render pass over that level and layer with
`vkCmdClearAttachments`. For BC images (`raw`) the rectangle is
converted to blocks.

`raw` is computed only for the BC range, so an image kept in ASTC blocks
was treated as uncompressed. Step by step:

1. On a device with native ASTC, the game clears an ASTC texture.
2. If the clear covers the whole level, `clear_tex` calls
`vkCmdClearColorImage` on the ASTC image. This breaks
**VUID-vkCmdClearColorImage-image-00007**: "image must not have a
compressed or depth/stencil format".
3. If it covers part of the level, the region path below only converts
the rectangle to blocks for BC formats, not for ASTC.
4. Either way, a colour written over ASTC blocks would be garbage, since
the blocks are compressed data, not texels.

### What changed (gpu/vk/vk_xfer.cpp, clear_tex)

Before, no ASTC check:

```cpp
full_barrier(cmd);
const bool volume = s.tex->type == VK_IMAGE_TYPE_3D;
const bool raw = s.tex->format >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && s.tex->format <= VK_FORMAT_BC7_SRGB_BLOCK;
```

After:

```cpp
full_barrier(cmd);
if (s.tex->format >= VK_FORMAT_ASTC_4x4_UNORM_BLOCK && s.tex->format <= VK_FORMAT_ASTC_12x12_SRGB_BLOCK) {
	warn(14, "clear of an ASTC texture: skipped");
	return;
}
const bool volume = s.tex->type == VK_IMAGE_TYPE_3D;
```

A clear of an image whose format is ASTC is skipped, and the image is
left as it was. `warn` logs it at most 3 times.

The early return also skips `texture_written` at the end of `clear_tex`,
so the texture is not marked as GPU-written. That is on purpose, because
nothing was written to it.

ASTC textures decoded to RGBA8, ASTC kept as BC3, and every other format
are cleared exactly as before.

### Who reaches this

Only devices that expose `textureCompressionASTC_LDR` and sample ASTC
natively, such as Intel iGPUs. AMD and NVIDIA desktop drivers don't
expose it, so on them the image is never in an ASTC format and the new
check never matches.

### Changes after review

- The description no longer claims that drivers hang. The rule comes
from the spec (the VUID above).
- Skipping `texture_written` was confirmed as intended.

### Test

There is no dedicated unit test. In the review run,
`test_bc_raw_targets.py` and `test_vulkan_clear_mip.py` pass with this
change.

### How tested

- Release build.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin), including about
40 minutes on nightly g57471723, which includes this change. Log clean,
and "clear of an ASTC texture" does not appear.
- This GPU has no native ASTC, so the new path was not run. The test
only checks that a GPU without native ASTC is unaffected.

Assisted-by: GitHub Copilot
### The bug

`mesh_args_take(n)` (gpu/vk/vk_draw.cpp) hands out `n` consecutive
16-byte entries of the mesh-argument ring (`kMeshArgs` entries). Each
indirect mesh draw gets its own range: the GPU copies the draw's counts
and firsts into it, and the draw reads them from there. The code was:

```cpp
const uint32_t at = next.fetch_add(n) % (kMeshArgs - n);
return at + 1;
```

The modulus depends on `n`, so two draws taken one after another with
different counts can land on the same entries. A worked example with the
old `kMeshArgs = 262144`:

1. The counter is at 262130, and a draw takes `n = 100`. The modulus is
262144 - 100 = 262044, so `at = 262130 % 262044 = 86`. The draw gets
entries **87..186**.
2. The counter is now 262230, and the next draw takes `n = 1`. The
modulus is 262143, so `at = 262230 % 262143 = 87`. The draw gets entry
**88**.
3. Entry 88 is inside the first draw's range. The second draw is a
direct draw with a first task, so it writes its first into entry 88 from
the CPU while recording (`mesh_args_words[at * 4 + 3] = first`). The
first draw's GPU copy, which runs later, writes its own count and first
over entry 88. The second draw then reads the first draw's values
instead of its own. Two indirect draws that overlap overwrite each other
in the same way, through their GPU copies.
4. On screen, this shows as wrong, flickering or missing mesh geometry.

Who calls `mesh_args_take`:

- **Without `RX_VK_MESH_ARGS` (the default):** only direct mesh draws
with a non-zero first task, always with `n = 1`. With a constant `n`,
the modulus is constant and nothing overlaps.
- **With `RX_VK_MESH_ARGS=1`:** the rewritten indirect draws also call
it, with `n = d.max` (from 1 up to the old `kMeshArgs - 1`). They mix
with the `n = 1` calls, and that mix is what overlaps.

### What changed (gpu/vk/vk_draw.cpp)

The modulus is now fixed at `kMeshArgs / 2`, and `n` is clamped to at
most `kMeshArgs / 2`:

```cpp
// before
const uint32_t at = next.fetch_add(n) % (kMeshArgs - n);
// after
n = std::clamp<uint32_t>(n, 1, kMeshArgs / 2);
const uint32_t at = next.fetch_add(n) % (kMeshArgs / 2);
```

- Every range starts in the first half of the ring and is at most half a
ring long, so it never runs past the end.
- With one modulus for every `n`, ranges handed out one after another
follow each other in the ring. An entry is only reused when the counter
has advanced by a whole modulus.
- The indirect caller clamps `n` the same way: `std::clamp(d.max, 1,
(int)(kMeshArgs / 2))` instead of `kMeshArgs - 1`.
That keeps the reuse distance at 262144 entries, as before; halving the
modulus alone would have halved it.
- A power-of-two modulus also stays continuous when the 32-bit counter
wraps, which the old one did not.

### Cost

- The ring buffer is host-visible and grows from 4 MB to 8 MB. It is
also created for direct mesh draws with a non-zero first task, flag or
not, so the extra 4 MB applies to everyone who reaches that path.
- The `RX_VK_LOG_MESH_PERPROG` diagnostic `prog_at` vector (`kMeshArgs`
entries) doubles too.

### Test

`tests/mesh_args_cleanup_test.cpp` checked that the ring allocation was
at most 4 MB, which the new size breaks. It now requires the allocation
to be exactly `kMeshArgs * 16` bytes, computed from the `kMeshArgs` the
test extracts from vk_draw.cpp, so it follows the real size instead of a
fixed bound. `tests/test_mesh_args_cleanup.py` passes its 5 cases.

### Changes after review

- The test fixture's 4 MB bound was replaced by the size computed from
`kMeshArgs`.
- The description now covers the extra 4 MB on the direct-draw path and
the doubled `prog_at` vector.

### How tested

- Release build.
- `tests/test_mesh_args_cleanup.py`: 5 cases pass.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin):
- With the flag off (the default; the `n = 1` direct-draw path),
including about 40 minutes on nightly g57471723, which includes this
change. Log clean.
- With `RX_VK_MESH_ARGS=1` (the rewritten indirect draws, mixed counts).

Assisted-by: GitHub Copilot
### The bug

`texel_buffer_view` (gpu/vk/vk_texture.cpp) creates the `VkBufferView`
for a texture the game binds as a texel buffer:

- as a **uniform** texel buffer when it is sampled,
- as a **storage** texel buffer when it is written.

The view's format is the one the texture maps to
(`vk_format(nf).format`). The only check was that the format is not
`VK_FORMAT_UNDEFINED`. Nothing checked whether the device supports texel
buffers of that format. Step by step:

1. The game binds a texture as a texel buffer, in a format such as
`VK_FORMAT_R32G32B32_SFLOAT`.
2. The device does not list `VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT`
(or `STORAGE_TEXEL_BUFFER_BIT`) for it in the format's `bufferFeatures`.
Which formats are listed varies by driver.
3. `texel_buffer_view` creates the view anyway. This is invalid usage:

| VUID | Text |
|---|---|
| VUID-VkBufferViewCreateInfo-format-08778 | If the buffer view usage
contains VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, then format features
of format must contain VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT |
| VUID-VkBufferViewCreateInfo-format-08779 | If the buffer view usage
contains VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT, then format features
of format must contain VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT |

4. What the shader then reads or writes through that view is undefined.

### What changed (gpu/vk/vk_texture.cpp, texel_buffer_view)

A check right after the format is known, before anything is created,
looked up, or marked as written:

```cpp
VkFormat f = vk_format(nf).format;
if (f == VK_FORMAT_UNDEFINED) return VK_NULL_HANDLE;
// new:
if (!(format_props(f).bufferFeatures & (storage ? VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT
												: VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT))) {
	static std::atomic<int> logged{0};
	if (logged.fetch_add(1, std::memory_order_relaxed) < 8)
		logf("vk: texel buffer of format %d (%s) not supported by the device: unbound", (int)f, storage ? "storage" : "uniform");
	return VK_NULL_HANDLE;
}
```

- A format without the needed feature gets no view, and the slot stays
unbound. Callers already handle a null view; it is the same result as
the existing `VK_FORMAT_UNDEFINED` case.
- Because it returns before `texture_gpu_wrote`, a storage bind that is
refused does not mark the texture as written by the GPU.
- The answer comes from the existing `format_props` cache, which takes
one short lock (`fmt_mu`) per texel-buffer bind but does not query the
driver again.
- The message is logged at most 8 times.
- Supported formats follow the same path as before.

### Test

`tests/test_texel_buffer_ownership.py` cuts `texel_buffer_view` out of
vk_texture.cpp and compiles it against the stubs in
`tests/texel_buffer_ownership_test.cpp`:

- The fixture gets a `format_props` stub. It reports both texel-buffer
features for every format except the one mapped from NVN format 44
(`VK_FORMAT_R32G32B32_SFLOAT`).
- A new case, `unsupported`, binds format 44 as uniform and as storage.
It checks that:
  - neither returns a view;
  - no view is created (`creates == 0`);
  - no buffer is looked up (`lookups == 0`);
  - the texture is not marked as written (`writes == 0`).
- All 24 cases pass. On main, the case list has 23.

### Changes after review

- The first version did not add the stub, so the test failed to compile
("'format_props' was not declared in this scope"). The stub and the
`unsupported` case were added.

### How tested

- Release build (`nx2/build.py --standalone`, Windows, llvm-mingw).
- `tests/test_texel_buffer_ownership.py`: 24 cases pass.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin):
  - Two full sessions on build g54d01844.
- One session of about 40 minutes on nightly g57471723, which includes
this change.
  - No regression.
- "texel buffer of format" does not appear in any of those logs. Every
texel-buffer format OoT uses is supported on this device, so the new
path is not reached there. This change guards against invalid usage; it
does not fix a visible glitch in OoT.

Assisted-by: GitHub Copilot
### The bug

At startup, `coopmat_init` (gpu/vk/vk_coopmat.cpp) checks whether native
matrix multiplies can be used with cooperative matrices. `measure_order`
and `test_multiply` each run a small compute shader through
`run_shader`. `run_shader` does the following:

1. Creates a host-visible buffer and its memory, maps it, and copies the
input into it.
2. Creates the pipeline, a descriptor pool, a command pool and a fence.
3. Submits one dispatch, waits for it for up to 5 s, and reads the
result back.
4. Destroys everything.

It had three problems.

**1. The mapping was not checked.**

```cpp
uint32_t* words = nullptr;
vkMapMemory(dev.device, memory, 0, size, 0, (void**)&words);
std::memcpy(words, data.data(), size);
```

If `vkMapMemory` failed (for example, out of host memory), `words`
stayed null and the `memcpy` crashed the emulator at startup.

**2. A dispatch that did not finish was destroyed under the GPU.**

```cpp
ok = vkQueueSubmit2(dev.queue, 1, &si, fence) == VK_SUCCESS &&
	 vkWaitForFences(dev.device, 1, &fence, VK_TRUE, 5000000000ull) == VK_SUCCESS;
...
vkDestroyFence(...); vkDestroyCommandPool(...); vkDestroyDescriptorPool(...); vkDestroyPipeline(...);
// and at the end of the function:
vkDestroyBuffer(...); vkFreeMemory(...);
```

When the submit succeeded but the wait timed out (a slow or hung
driver), the code still destroyed everything while the GPU could still
be running the dispatch. Each of those calls breaks a rule:

| VUID | Text |
|---|---|
| VUID-vkDestroyFence-fence-01120 | All queue submission commands that
refer to fence must have completed execution |
| VUID-vkDestroyCommandPool-commandPool-00041 | All VkCommandBuffer
objects allocated from commandPool must not be in the pending state |
| VUID-vkDestroyDescriptorPool-descriptorPool-00303 | All submitted
commands that refer to descriptorPool (via any allocated descriptor
sets) must have completed execution |
| VUID-vkDestroyPipeline-pipeline-00765 | All submitted commands that
refer to pipeline must have completed execution |
| VUID-vkDestroyBuffer-buffer-00922 | All submitted commands that refer
to buffer, either directly or via a VkBufferView, must have completed
execution |
| VUID-vkFreeMemory-memory-00677 | All submitted commands that refer to
memory (via images or buffers) must have completed execution |

**3. The fence was created unchecked.**

If `vkCreateFence` failed, the fence was a null handle, and the code
still submitted with it and then waited on it. `vkWaitForFences`
requires valid handles (VUID-vkWaitForFences-pFences-parameter: "pFences
must be a valid pointer to an array of fenceCount valid VkFence
handles").

### What changed (gpu/vk/vk_coopmat.cpp, run_shader)

- A failed mapping now logs "cooperative matrix test: the buffer could
not be mapped", frees the buffer and memory, and fails the probe:

  ```cpp
if (vkMapMemory(dev.device, memory, 0, size, 0, (void**)&words) !=
VK_SUCCESS || !words) {
	  logf("vk: cooperative matrix test: the buffer could not be mapped");
	  vkDestroyBuffer(dev.device, buffer, nullptr);
	  vkFreeMemory(dev.device, memory, nullptr);
	  return false;
  }
  ```

- A fence that cannot be created makes the probe skip the submit.
Nothing was queued, so everything is freed as usual and the probe fails:

  ```cpp
if (vkCreateFence(dev.device, &fci, nullptr, &fence) != VK_SUCCESS)
fence = VK_NULL_HANDLE;
const bool submitted = fence && vkQueueSubmit2(dev.queue, 1, &si, fence)
== VK_SUCCESS;
ok = submitted && vkWaitForFences(dev.device, 1, &fence, VK_TRUE,
5000000000ull) == VK_SUCCESS;
  ```

- A dispatch that was submitted but did not complete leaves the fence,
command pool, descriptor pool, pipeline, buffer and memory alive,
instead of destroying them under the GPU. Only the shader module is
destroyed, which is allowed once the pipeline exists. This is a one-time
leak at startup; on a device that really hung, later submits would queue
behind it anyway.

  ```cpp
if (submitted && !ok) { // (it may still be running: what it uses is
left alive rather than destroyed under it)
	  if (module) vkDestroyShaderModule(dev.device, module, nullptr);
	  return false;
  }
  ```

The probe's result and every successful path are unchanged. A probe that
fails falls back to software matrix multiplies, as before.

### Note for #1975

#1975 (cooperative matrices for the AA networks) changes `run_shader`'s
signature. It merges cleanly with this PR, but whichever lands second
should check that the new early returns still free what #1975 adds.

### Test

There is no dedicated unit test. The failure paths need a driver that
fails `vkMapMemory` or `vkCreateFence`, or a dispatch that takes more
than 5 s.

### How tested

- Release build.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin), including about
40 minutes on nightly g57471723, which includes this change. Log clean.
- On this GPU, `coopmat_init` stops before the probe: the log says
"matrix multiplies in software (the cooperative matrices lack 16x8x16
f16)". So `run_shader` is not reached there, and none of the changed
paths ran. The test only shows that startup is unaffected.

Assisted-by: GitHub Copilot
### The bug

A sparse texture gets device memory per block (tile) as it is written,
instead of all at once (gpu/vk/vk_texture.cpp):

- `sparse_queue` walks the tiles a write reaches. For each tile that has
no memory yet, it takes memory with `sparse_take`, appends a bind to
`sparse_binds`, and **marks the tile bound at once** (`bound = 1`). The
mip tail works the same way, with `sparse_opaque` and `tail_bound`.
- `sparse_flush` then binds the whole list with `vkQueueBindSparse` and
waits on a fence.

When the bind failed, `sparse_flush` only logged it:

```cpp
res = vkQueueBindSparse(dev.queue, 1, &bi, fence);
...
if (res == VK_SUCCESS) {
	vkWaitForFences(...); vkResetFences(...);
} else {
	logf("vk: sparse texture %#llx: binding failed (%d)", ...);
}
...
sparse_binds.clear(); sparse_opaque.clear();
return res == VK_SUCCESS;
```

Step by step:

1. A write to a sparse texture queues the binds of tiles A, B and C.
Their `tiles[...]` entries are set to 1.
2. `vkQueueBindSparse` fails (for example, out of memory). The list is
cleared, but the tiles stay marked bound.
3. The upload's copies go ahead and write tiles A, B and C, which have
no memory behind them.
4. Every later write to those tiles sees them marked bound, so it never
queues them again. Those tiles stay unbacked for the life of the
texture, and every write to them is lost.

Two more problems were in the same function:

- The fence was created without checking. If `vkCreateFence` failed,
every bind was submitted and waited on with a null fence.
`vkWaitForFences` requires valid handles
(VUID-vkWaitForFences-pFences-parameter: "pFences must be a valid
pointer to an array of fenceCount valid VkFence handles").
- The memory `sparse_take` gave for the failed binds was simply lost to
that texture.

### What changed (gpu/vk/vk_texture.cpp and vk_texture.h)

- **New `sparse_unqueue(tex)`.** When the binds are not made, it:
- marks every queued tile unbound again (`map[i] = 0`), finding the tile
with the same math `sparse_queue` uses;
  - marks every queued mip tail unbound again (`tail_bound[layer] = 0`);
  - puts the memory those binds had taken on the texture's free list;
  - clears the queue, and counts `sparse_added` as `sparse_flush` does.

The next write to those tiles queues them again, so the bind is retried.

- **`sparse_flush` calls it in both failure cases**, and returns false:

  ```cpp
  if (vkCreateFence(dev.device, &fi, nullptr, &fence) != VK_SUCCESS) {
	  fence = VK_NULL_HANDLE;
	  static int logged;
if (logged++ < 8) logf("vk: sparse texture %#llx: no fence for its
binds", ...);
	  sparse_unqueue(tex);
	  return false;
  }
  ...
  } else {
	  static int failed;
if (failed++ < 8) logf("vk: sparse texture %#llx: binding failed (%d)",
...);
	  sparse_unqueue(tex);
	  return false;
  }
  ```

- **New per-texture free list** (`Texture::sparse_free`, with memory,
offset and size). `sparse_take` gives it out before taking new chunk
memory. A bind that keeps failing reuses the same memory instead of
taking new memory on every retry. The list only points at the texture's
own chunks, which are freed with the texture.

- **Both messages are capped at 8.** A failing bind is now retried on
later writes, so without the cap it would log on every write to that
texture.

The success path is unchanged.

### Who reaches this

A texture is only sparse when it is a 2D array larger than
`RX_VK_TEX_MAX_MB` (1.5 GB) on a device with sparse support. That is
MKW's course-map case. OoT likely never makes one.

### Changes after review

- The "binding failed" and "no fence for its binds" messages are capped
at 8, like the other sparse messages.
- The description now says when a texture is sparse, and that the OoT
test does not reach this path.

### Test

There is no dedicated unit test. The failure needs `vkQueueBindSparse`
or `vkCreateFence` to fail on a sparse texture.

### How tested

- Release build.
- Zelda OoT on Radeon RX 9070 XT (Windows, Adrenalin), including about
40 minutes on nightly g57471723, which includes this change. Log clean,
with no "sparse texture" line.
- The sparse path itself was not run. The test only shows that
non-sparse textures are unaffected.

Assisted-by: GitHub Copilot
test: stand-ins for the texture views' front cache (#2984) and Compile shaders (compile_all) in the checks that cut them
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 29s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
513646e958
#2984 clears view_fast() with handle_hits() in draw_begin_recording, and three checks that cut that function had no
ViewFast; Longshot's start sequence asks compile_all() before a prediction, and #2982's check of the two step
sequences had no stand-in for it. Both broke only the checks' builds.

Co-authored-by: Cursor <[email protected]>
TrashPanda force-pushed restore/github-after-split from 513646e958
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 29s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 75ca015d11
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 11s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
2026-11-13 14:40:17 +00:00
Compare
TrashPanda force-pushed restore/github-after-split from 75ca015d11
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 11s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 7a479474a4
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
2026-11-13 14:44:16 +00:00
Compare
TrashPanda force-pushed restore/github-after-split from 7a479474a4
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to c5e19e1c8b
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
2026-11-13 14:44:39 +00:00
Compare
TrashPanda force-pushed restore/github-after-split from c5e19e1c8b
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 7bf35db418
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
2026-11-13 14:45:42 +00:00
Compare
TrashPanda changed title from WIP: chore: restore 24 Hookshot PRs merged on GitHub after Longshot branched to WIP: chore: restore 81 Hookshot PRs merged on GitHub after Longshot branched 2026-11-13 14:46:07 +00:00
HumanBean left a comment

Thanks for this. Bringing back the 81 merged Hookshot fixes Longshot lost when it branched is worth doing, and the PR is careful about it. Each restored PR is its own commit, under its original author (29 of them), with the upstream title and body. The 9 left out are each listed with a reason, and I agree with all of them: Hookshot #2972 and Hookshot #3038 are already in Longshot, the four Builder-window ones went with #181, Hookshot #2946 and Hookshot #2926 are covered on Longshot's side, and Hookshot #2582 needs a real port onto Longshot's cap. Some commits needed a port, and 7 of them say how in a "Ported to Longshot:" note.

I read the PR-only diff against main@origin (fork point 2331f46c), the commit messages, and the renderer, host and setup commits in particular. I didn't build, run or compare with upstream, since GitHub is down.

Checked, and fine as far as I can tell:

Must change

  1. Drop 90bfb06a (https://github.com/NotRexed/Hookshot/pull/2732, scripts/dev_packs.py). Main already has it, as #147 (fdc2840b). Main's version is newer: its CACHES, WRITTEN_IN_PLAY and NO_MODULE cover more (including .forgejo/), and its prepare() handles a stalled driver. This commit adds the older 695-line file over it, along with a second "Testing a change against a base" section in docs/development.md and a duplicate line in AGENTS.md. A rebase will conflict on all four files, and taking this side would undo #147.
  2. Rebase onto current main, which also makes "Rebased on current main" true. Right now the box is ticked but false: main has moved five commits since 2331f46c (#214, #197, #147, #78, #54). Besides the dev_packs conflict, #197 changed vk_replay.cpp, vk_draw.cpp, vk_main.cpp, vk_internal.h and tests/fsr_uniform_ownership_test.cpp, all of which this PR also edits. Re-run run_all.py after the rebase.
  3. Build the branch for Windows and Linux before it goes out of WIP. About 60 commits change C and C++ in the renderer, the host and the setup, and nothing has compiled them together on Longshot. The checks only compile the functions they cut out. For an rc, at least one full build on each platform and a short run in the game are needed. Upstream measurements were taken on Hookshot's tree, not this one.

Should change

  1. Split it before it merges. One PR of 81 commits can't be reviewed or reverted as a unit, and the "One topic" box is honestly unticked. Here's one way to cut it, from safest to riskiest:

    If you split it, Hookshot #2939 and Hookshot #2989 each change the interface, so each piece needs its own MKWVK_API_VERSION bump (22, then 23), not one shared 22.

  2. Each Hookshot reference needs its full URL. Without one, #NNNN links to Longshot's own number:

    • The PR description uses bare #2949 and similar for Hookshot PRs, mixed in with Longshot's real #77, #181 and #215. Use https://github.com/NotRexed/Hookshot/pull/NNNN for the Hookshot ones.
    • The commit messages have Fixes #2180, #2182, #2220, #2579, #2954, #2885 and #2667 (in Hookshot #2766, Hookshot #3044, Hookshot #2939, Hookshot #2625, Hookshot #2978, Hookshot #2933 and Hookshot #2929). Forgejo reads those as closing Longshot issues once Longshot's numbers get there. Reword them, or add a Restored-from: https://github.com/NotRexed/Hookshot/pull/NNNN trailer to each commit. A trailer would also give the commits-to-Hookshot mapping a home outside this description.
    • New text in the diff has bare references too. The worst is docs/test-requests.md's "Hookshot's PR #2731" (should be https://github.com/NotRexed/Hookshot/pull/2731). Others are in docs/tested-configurations.md (9 rows), tests/srccut.py, vk_memory.cpp and vk_texture.cpp.
  3. The staging prewarm (https://github.com/NotRexed/Hookshot/pull/2960) doesn't keep what it makes. The 4 × 64 MB arenas made in replay_start go through texture_frame's existing keep and idle rules. On a card with 6.5 GB or less, keep is 64 MB, so three of the four are freed at the first frame. On every card, all of them are freed after 30 s untouched. So it pays 256 MB of host-visible memory at start for little lasting effect, and the title's "keep" isn't implemented. Either mark the prewarmed arenas as kept, or leave this part out of rc.4. Inline comment below.

  4. Small description fixes:

    • "juliotec's seven Vulkan robustness fixes" lists eight PRs, and there are eight juliotec commits.
    • HOS_FS_OVERLAY is in the flags table as if it were new, but it isn't.
    • Hookshot #2559's NVIDIA default comes with no Longshot measurement. For rc.4, say what it was measured on and what it risks: dispatches back to back get no barrier of their own.

Not checked:

Verdict: request changes. Items 1 to 3 have to be fixed before this can merge, and it's still WIP.

Thanks for this. Bringing back the 81 merged Hookshot fixes Longshot lost when it branched is worth doing, and the PR is careful about it. Each restored PR is its own commit, under its original author (29 of them), with the upstream title and body. The 9 left out are each listed with a reason, and I agree with all of them: [Hookshot #2972](https://github.com/NotRexed/Hookshot/pull/2972) and [Hookshot #3038](https://github.com/NotRexed/Hookshot/pull/3038) are already in Longshot, the four Builder-window ones went with [#181](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/181), [Hookshot #2946](https://github.com/NotRexed/Hookshot/pull/2946) and [Hookshot #2926](https://github.com/NotRexed/Hookshot/pull/2926) are covered on Longshot's side, and [Hookshot #2582](https://github.com/NotRexed/Hookshot/pull/2582) needs a real port onto Longshot's cap. Some commits needed a port, and 7 of them say how in a "Ported to Longshot:" note. I read the PR-only diff against `main@origin` (fork point `2331f46c`), the commit messages, and the renderer, host and setup commits in particular. I didn't build, run or compare with upstream, since GitHub is down. **Checked, and fine as far as I can tell:** - `MKWVK_API_VERSION` goes 21 → 22 for the two interface changes: `MkwVkHost.wide_cover` (https://github.com/NotRexed/Hookshot/pull/2939) and `HOS_PAD_TOUCHPAD` moving inside `HOS_PAD_N` (https://github.com/NotRexed/Hookshot/pull/2989). `pad_name`, `touchpad_bound`, `nn_hid.c`'s range checks and `prompts.c`'s `HOS_PAD_ART_N` fall-back all agree on the new value. `HOS_OPT_AUTO_RUN = 55` doesn't collide with another option. - `fg_scale_comp_spv.h` and `fg_scale_pq_comp_spv.h` name the SHA-256 of `fg_scale.comp` as it is at the PR (`6c105b2d…`). `third_party/` isn't touched. The `gpu/vk/sirit` and `gpu/vk/eden` edits only drop "(super build)" tags (https://github.com/NotRexed/Hookshot/pull/2782), and those folders aren't under the provenance check. - `tests/srccut.py` (https://github.com/NotRexed/Hookshot/pull/2914) makes the 80 checks it touches stricter: a missing or repeated marker now fails. I sampled `test_vsync_row.py`, `test_commit_limit.py`, `test_low_vram_texture_mips.py` and the stand-ins commit `7bf35db4`, and none of them is weakened. The Windows `unsetenv` shim is still there. - Several Longshot-specific pieces carried through intact: - `HOS_FS_OVERLAY` and `thread_arg`'s own-thread fast path (https://github.com/NotRexed/Hookshot/pull/2708). The condition-variable early return counts a waiter in before `cv->lk`, so it misses no wake-up the old Signal would have given. - The commit-limit reserve (https://github.com/NotRexed/Hookshot/pull/3017), which builds on Longshot's [#77](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/77) rather than duplicating it. - The setup's `move_retry` (https://github.com/NotRexed/Hookshot/pull/3021): `fail` doesn't return, and `publish_install` restores `GetLastError`. - The SDL audio close (https://github.com/NotRexed/Hookshot/pull/2970). Every caller handles the new `void *` return. - The Linux ReplayPin affinity reset (https://github.com/NotRexed/Hookshot/pull/2903). - The Intel split-submission default (https://github.com/NotRexed/Hookshot/pull/3000). - The camera-cut detector (https://github.com/NotRexed/Hookshot/pull/2927). - The juliotec read-back and sparse-bind fixes (https://github.com/NotRexed/Hookshot/pull/3032, https://github.com/NotRexed/Hookshot/pull/2743). - The scarecrow kind-11 line (https://github.com/NotRexed/Hookshot/pull/3018) and main's newer Scarecrow timing fix [#78](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/78) touch different parts of `nx2/oot_patches.py`. They don't overlap. ## Must change 1. **Drop `90bfb06a` (https://github.com/NotRexed/Hookshot/pull/2732, `scripts/dev_packs.py`). Main already has it, as [#147](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/147) (`fdc2840b`).** Main's version is newer: its `CACHES`, `WRITTEN_IN_PLAY` and `NO_MODULE` cover more (including `.forgejo/`), and its `prepare()` handles a stalled driver. This commit adds the older 695-line file over it, along with a second "Testing a change against a base" section in `docs/development.md` and a duplicate line in `AGENTS.md`. A rebase will conflict on all four files, and taking this side would undo [#147](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/147). 2. **Rebase onto current `main`, which also makes "Rebased on current `main`" true.** Right now the box is ticked but false: main has moved five commits since `2331f46c` ([#214](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/214), [#197](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/197), [#147](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/147), [#78](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/78), [#54](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/54)). Besides the dev_packs conflict, [#197](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/197) changed `vk_replay.cpp`, `vk_draw.cpp`, `vk_main.cpp`, `vk_internal.h` and `tests/fsr_uniform_ownership_test.cpp`, all of which this PR also edits. Re-run `run_all.py` after the rebase. 3. **Build the branch for Windows and Linux before it goes out of WIP.** About 60 commits change C and C++ in the renderer, the host and the setup, and nothing has compiled them together on Longshot. The checks only compile the functions they cut out. For an rc, at least one full build on each platform and a short run in the game are needed. Upstream measurements were taken on Hookshot's tree, not this one. ## Should change 4. **Split it before it merges.** One PR of 81 commits can't be reviewed or reverted as a unit, and the "One topic" box is honestly unticked. Here's one way to cut it, from safest to riskiest: - **a. Tools, checks and docs.** No effect on the game, so the game-free checks are enough: - [Hookshot #2949](https://github.com/NotRexed/Hookshot/pull/2949), [Hookshot #2887](https://github.com/NotRexed/Hookshot/pull/2887), [Hookshot #2914](https://github.com/NotRexed/Hookshot/pull/2914), [Hookshot #2877](https://github.com/NotRexed/Hookshot/pull/2877), [Hookshot #2866](https://github.com/NotRexed/Hookshot/pull/2866), [Hookshot #2714](https://github.com/NotRexed/Hookshot/pull/2714) and [Hookshot #2703](https://github.com/NotRexed/Hookshot/pull/2703) - the README form-feed fix ([Hookshot #2553](https://github.com/NotRexed/Hookshot/pull/2553)) - the 11 tested-configuration rows - **b. [Hookshot #2830](https://github.com/NotRexed/Hookshot/pull/2830) on its own.** It changes `scripts/test_requests.py`'s commands for the whole team, so it should merge separately. - **c. Player-facing fixes, small and low risk. Worth taking for v3.0.0-rc.4:** - Commit-limit crash: [Hookshot #3017](https://github.com/NotRexed/Hookshot/pull/3017) - Input: - Mouse 4/5 on Linux: [Hookshot #2961](https://github.com/NotRexed/Hookshot/pull/2961) - A control's second key: [Hookshot #2933](https://github.com/NotRexed/Hookshot/pull/2933) - Background input and gyro while the menu is open: [Hookshot #2944](https://github.com/NotRexed/Hookshot/pull/2944), [Hookshot #2962](https://github.com/NotRexed/Hookshot/pull/2962) and [Hookshot #2963](https://github.com/NotRexed/Hookshot/pull/2963) - Mouse camera: [Hookshot #2947](https://github.com/NotRexed/Hookshot/pull/2947) - Touchpad binding: [Hookshot #2989](https://github.com/NotRexed/Hookshot/pull/2989) (API bump) - Menu saves and bug reports: [Hookshot #2966](https://github.com/NotRexed/Hookshot/pull/2966), [Hookshot #2948](https://github.com/NotRexed/Hookshot/pull/2948) and [Hookshot #2978](https://github.com/NotRexed/Hookshot/pull/2978) - Audio: Linux sound reopen ([Hookshot #2970](https://github.com/NotRexed/Hookshot/pull/2970)) and 5.1 speakers ([Hookshot #3044](https://github.com/NotRexed/Hookshot/pull/3044)) - Game patches: scarecrow ([Hookshot #3018](https://github.com/NotRexed/Hookshot/pull/3018)) and floor web above 60 fps ([Hookshot #2929](https://github.com/NotRexed/Hookshot/pull/2929)) - Builder and setup: [Hookshot #2953](https://github.com/NotRexed/Hookshot/pull/2953), [Hookshot #2968](https://github.com/NotRexed/Hookshot/pull/2968), [Hookshot #2985](https://github.com/NotRexed/Hookshot/pull/2985) and [Hookshot #3021](https://github.com/NotRexed/Hookshot/pull/3021) - The renderer load error on Linux: [Hookshot #2875](https://github.com/NotRexed/Hookshot/pull/2875) - **d. Renderer fixes.** Also good for rc.4, but they need an in-game run: - juliotec's eight: [Hookshot #2691](https://github.com/NotRexed/Hookshot/pull/2691), [Hookshot #2718](https://github.com/NotRexed/Hookshot/pull/2718), [Hookshot #2719](https://github.com/NotRexed/Hookshot/pull/2719), [Hookshot #2741](https://github.com/NotRexed/Hookshot/pull/2741), [Hookshot #2742](https://github.com/NotRexed/Hookshot/pull/2742), [Hookshot #2743](https://github.com/NotRexed/Hookshot/pull/2743), [Hookshot #3031](https://github.com/NotRexed/Hookshot/pull/3031) and [Hookshot #3032](https://github.com/NotRexed/Hookshot/pull/3032) - Textures: [Hookshot #3003](https://github.com/NotRexed/Hookshot/pull/3003) and [Hookshot #3002](https://github.com/NotRexed/Hookshot/pull/3002) - FSR 4 tail write order: [Hookshot #2952](https://github.com/NotRexed/Hookshot/pull/2952) - Intel split submissions: [Hookshot #3000](https://github.com/NotRexed/Hookshot/pull/3000) - Camera cuts: [Hookshot #2927](https://github.com/NotRexed/Hookshot/pull/2927) - Ultrawide loading cover: [Hookshot #2939](https://github.com/NotRexed/Hookshot/pull/2939) (API bump and shader) - FSR 3.1 HDR: [Hookshot #2614](https://github.com/NotRexed/Hookshot/pull/2614) - Pipelines: [Hookshot #2982](https://github.com/NotRexed/Hookshot/pull/2982), [Hookshot #2983](https://github.com/NotRexed/Hookshot/pull/2983) and [Hookshot #2987](https://github.com/NotRexed/Hookshot/pull/2987) - **e. New defaults and speed-ups.** I'd hold these until after rc.4, or until they have Longshot numbers: - ASTC and texture memory: BC1 for opaque ASTC ([Hookshot #2554](https://github.com/NotRexed/Hookshot/pull/2554)), the Texture detail row ([Hookshot #2557](https://github.com/NotRexed/Hookshot/pull/2557)), 32 MB blocks ([Hookshot #2967](https://github.com/NotRexed/Hookshot/pull/2967)), the 7/8 trim ([Hookshot #3020](https://github.com/NotRexed/Hookshot/pull/3020)) - Barrier-ordered passes, on NVIDIA by default: [Hookshot #2559](https://github.com/NotRexed/Hookshot/pull/2559) - Staging, views and memory: [Hookshot #2960](https://github.com/NotRexed/Hookshot/pull/2960), [Hookshot #2469](https://github.com/NotRexed/Hookshot/pull/2469), [Hookshot #2476](https://github.com/NotRexed/Hookshot/pull/2476), [Hookshot #2482](https://github.com/NotRexed/Hookshot/pull/2482), [Hookshot #2766](https://github.com/NotRexed/Hookshot/pull/2766), [Hookshot #2785](https://github.com/NotRexed/Hookshot/pull/2785) and [Hookshot #3043](https://github.com/NotRexed/Hookshot/pull/3043) - Condition-variable fast path: [Hookshot #2708](https://github.com/NotRexed/Hookshot/pull/2708) - Narrow stores: [Hookshot #2976](https://github.com/NotRexed/Hookshot/pull/2976) - ReplayPin on Linux by default: [Hookshot #2903](https://github.com/NotRexed/Hookshot/pull/2903) - Pipelines and statistics: [Hookshot #2625](https://github.com/NotRexed/Hookshot/pull/2625), [Hookshot #2782](https://github.com/NotRexed/Hookshot/pull/2782), [Hookshot #2896](https://github.com/NotRexed/Hookshot/pull/2896) and [Hookshot #2897](https://github.com/NotRexed/Hookshot/pull/2897) - The two shipped pipeline lists: [Hookshot #3029](https://github.com/NotRexed/Hookshot/pull/3029) and [Hookshot #3030](https://github.com/NotRexed/Hookshot/pull/3030) - Auto run: [Hookshot #2747](https://github.com/NotRexed/Hookshot/pull/2747) If you split it, [Hookshot #2939](https://github.com/NotRexed/Hookshot/pull/2939) and [Hookshot #2989](https://github.com/NotRexed/Hookshot/pull/2989) each change the interface, so each piece needs its own `MKWVK_API_VERSION` bump (22, then 23), not one shared 22. 5. **Each Hookshot reference needs its full URL.** Without one, `#NNNN` links to Longshot's own number: - **The PR description** uses bare `#2949` and similar for Hookshot PRs, mixed in with Longshot's real `#77`, `#181` and `#215`. Use `https://github.com/NotRexed/Hookshot/pull/NNNN` for the Hookshot ones. - **The commit messages** have `Fixes #2180`, `#2182`, `#2220`, `#2579`, `#2954`, `#2885` and `#2667` (in [Hookshot #2766](https://github.com/NotRexed/Hookshot/pull/2766), [Hookshot #3044](https://github.com/NotRexed/Hookshot/pull/3044), [Hookshot #2939](https://github.com/NotRexed/Hookshot/pull/2939), [Hookshot #2625](https://github.com/NotRexed/Hookshot/pull/2625), [Hookshot #2978](https://github.com/NotRexed/Hookshot/pull/2978), [Hookshot #2933](https://github.com/NotRexed/Hookshot/pull/2933) and [Hookshot #2929](https://github.com/NotRexed/Hookshot/pull/2929)). Forgejo reads those as closing Longshot issues once Longshot's numbers get there. Reword them, or add a `Restored-from: https://github.com/NotRexed/Hookshot/pull/NNNN` trailer to each commit. A trailer would also give the commits-to-Hookshot mapping a home outside this description. - **New text in the diff** has bare references too. The worst is `docs/test-requests.md`'s "Hookshot's PR #2731" (should be https://github.com/NotRexed/Hookshot/pull/2731). Others are in `docs/tested-configurations.md` (9 rows), `tests/srccut.py`, `vk_memory.cpp` and `vk_texture.cpp`. 6. **The staging prewarm (https://github.com/NotRexed/Hookshot/pull/2960) doesn't keep what it makes.** The 4 × 64 MB arenas made in `replay_start` go through `texture_frame`'s existing keep and idle rules. On a card with 6.5 GB or less, `keep` is 64 MB, so three of the four are freed at the first frame. On every card, all of them are freed after 30 s untouched. So it pays 256 MB of host-visible memory at start for little lasting effect, and the title's "keep" isn't implemented. Either mark the prewarmed arenas as kept, or leave this part out of rc.4. Inline comment below. 7. **Small description fixes:** - "juliotec's seven Vulkan robustness fixes" lists eight PRs, and there are eight juliotec commits. - `HOS_FS_OVERLAY` is in the flags table as if it were new, but it isn't. - [Hookshot #2559](https://github.com/NotRexed/Hookshot/pull/2559)'s NVIDIA default comes with no Longshot measurement. For rc.4, say what it was measured on and what it risks: dispatches back to back get no barrier of their own. Not checked: - most of `vk_draw.cpp`: [Hookshot #2625](https://github.com/NotRexed/Hookshot/pull/2625), [Hookshot #2896](https://github.com/NotRexed/Hookshot/pull/2896), [Hookshot #2983](https://github.com/NotRexed/Hookshot/pull/2983) and [Hookshot #2987](https://github.com/NotRexed/Hookshot/pull/2987) - the target-overlap logic in [Hookshot #2766](https://github.com/NotRexed/Hookshot/pull/2766) - the ASTC→BC1 encoder in [Hookshot #2554](https://github.com/NotRexed/Hookshot/pull/2554) - the eden narrow-store emitter in [Hookshot #2976](https://github.com/NotRexed/Hookshot/pull/2976) - `nn_hid.c`'s input changes in [Hookshot #2944](https://github.com/NotRexed/Hookshot/pull/2944), [Hookshot #2962](https://github.com/NotRexed/Hookshot/pull/2962) and [Hookshot #2963](https://github.com/NotRexed/Hookshot/pull/2963), beyond reading the diff - `longshot_bench.py` and the [Hookshot #2830](https://github.com/NotRexed/Hookshot/pull/2830) port of `test_requests.py` - the lavapipe CI job - the two pipeline-list binaries - any comparison with upstream's merged versions Verdict: request changes. Items 1 to 3 have to be fixed before this can merge, and it's still WIP.
@ -94,0 +114,4 @@
### Which commit?
`request` pins the PR's head at that moment. For example, on November 12, Hookshot's PR #2731 moved from `bce3d17e` to
Owner

The bare #2731 links to Longshot's own PR 2731 here. Please write it as https://github.com/NotRexed/Hookshot/pull/2731, and do the same for the other Hookshot numbers this PR adds to docs/, comments and the description.

The bare `#2731` links to Longshot's own PR 2731 here. Please write it as https://github.com/NotRexed/Hookshot/pull/2731, and do the same for the other Hookshot numbers this PR adds to `docs/`, comments and the description.
@ -16,3 +16,3 @@
#endif
#define MKWVK_API_VERSION 21
#define MKWVK_API_VERSION 22
Owner

22 covers both interface changes here: MkwVkHost.wide_cover (https://github.com/NotRexed/Hookshot/pull/2939) and HOS_PAD_TOUCHPAD moving inside HOS_PAD_N (https://github.com/NotRexed/Hookshot/pull/2989). That's right while they stay in one PR. If the PR is split, whichever piece merges second needs its own bump to 23.

22 covers both interface changes here: `MkwVkHost.wide_cover` (https://github.com/NotRexed/Hookshot/pull/2939) and `HOS_PAD_TOUCHPAD` moving inside `HOS_PAD_N` (https://github.com/NotRexed/Hookshot/pull/2989). That's right while they stay in one PR. If the PR is split, whichever piece merges second needs its own bump to 23.
@ -518,0 +578,4 @@
had gone stale and been given back, so the first upload of the next burst made another. On an empty device they are cheap.
RX_VK_STAGING_PREWARM=<n>: how many, 0: none. */
} // namespace (the prewarm is called from the replay's start: it has to link)
void texture_staging_prewarm(int n) {
Owner

These arenas don't survive long. They go through texture_frame's existing free loop (if (idle && (it->size > kArena || kept + it->size > keep || stale))). On a card with 6.5 GB or less, keep is 64 MB, so three of the four are freed at the first frame. On every card, all of them are freed 30 s after used_at, which is set here. So the commit title's "keep the staging arenas" isn't implemented, and on small cards this costs 256 MB of host-visible (BAR) memory at start for nothing. Either exempt the prewarmed ones from the keep and idle rules, for example with a flag on Arena, or leave this half of https://github.com/NotRexed/Hookshot/pull/2960 out for rc.4.

These arenas don't survive long. They go through `texture_frame`'s existing free loop (`if (idle && (it->size > kArena || kept + it->size > keep || stale))`). On a card with 6.5 GB or less, `keep` is 64 MB, so three of the four are freed at the first frame. On every card, all of them are freed 30 s after `used_at`, which is set here. So the commit title's "keep the staging arenas" isn't implemented, and on small cards this costs 256 MB of host-visible (BAR) memory at start for nothing. Either exempt the prewarmed ones from the keep and idle rules, for example with a flag on `Arena`, or leave this half of https://github.com/NotRexed/Hookshot/pull/2960 out for rc.4.
@ -2347,0 +2561,4 @@
bool game_order() {
static const bool on = [] {
const char* e = getenv("RX_VK_GAME_ORDER");
const bool v = e ? !env_value_off(e) : dev.props.vendorID == 0x10de;
Owner

This is a new default for every NVIDIA player: back-to-back passes and direct dispatches get no barrier of their own (https://github.com/NotRexed/Hookshot/pull/2559). The PR has no Longshot measurement for it. For rc.4, please say what it was measured on and what it costs if the game's barriers miss a hazard, or hold it for after rc.4.

This is a new default for every NVIDIA player: back-to-back passes and direct dispatches get no barrier of their own (https://github.com/NotRexed/Hookshot/pull/2559). The PR has no Longshot measurement for it. For rc.4, please say what it was measured on and what it costs if the game's barriers miss a hazard, or hold it for after rc.4.
@ -0,0 +1,695 @@
"""Development builds as a base and small packs over it: build the game once, then build only what a change touches and run it
Owner

Main already has this script, as #147 (fdc2840b), and main's version is newer. Its CACHES and WRITTEN_IN_PLAY cover the whole cache family, NO_MODULE includes .forgejo/, and prepare() handles a stalled driver (PREPARE_FAILED). This commit (Hookshot https://github.com/NotRexed/Hookshot/pull/2732) would conflict with it here, in tests/test_dev_packs.py, in docs/development.md and in AGENTS.md, and taking this side would undo #147. Please drop 90bfb06a from the branch.

Main already has this script, as [#147](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/147) (`fdc2840b`), and main's version is newer. Its `CACHES` and `WRITTEN_IN_PLAY` cover the whole cache family, `NO_MODULE` includes `.forgejo/`, and `prepare()` handles a stalled driver (`PREPARE_FAILED`). This commit (Hookshot https://github.com/NotRexed/Hookshot/pull/2732) would conflict with it here, in `tests/test_dev_packs.py`, in `docs/development.md` and in `AGENTS.md`, and taking this side would undo #147. Please drop `90bfb06a` from the branch.
Contributor

Thanks for restoring these, TrashPanda. Three of the commits here are already on Longshot from me, or in an open PR, and HumanBean's review of #156 asked us to settle which PR carries them:

  • bd2b56d3 (Hookshot #2708, three per-call costs) is the same change as #156 (WIP, same code and tests; #156 adds a docs/development.md row and is waiting for a Longshot A/B). I'm happy for #209 to carry it: I'd close #156 and post the docs row and the Longshot measurement here as a comment for you to take or leave. Or, if you'd rather keep #209 to what has no Longshot PR yet, drop bd2b56d3 and #156 carries it. Your call.
  • 3a04664c (Hookshot #2896, native shader keys in draw statistics) is #169 (WIP, refreshed onto current main with a fixture that fails on the old source). Same question; I'll follow whichever you prefer.
  • 90bfb06a (Hookshot #2732, dev_packs) is already merged as #147 (fdc2840b, with fixes from its review), so it can be dropped here.
Thanks for restoring these, TrashPanda. Three of the commits here are already on Longshot from me, or in an open PR, and HumanBean's review of #156 asked us to settle which PR carries them: - **bd2b56d3** (Hookshot #2708, three per-call costs) is the same change as https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/156 (WIP, same code and tests; #156 adds a docs/development.md row and is waiting for a Longshot A/B). I'm happy for #209 to carry it: I'd close #156 and post the docs row and the Longshot measurement here as a comment for you to take or leave. Or, if you'd rather keep #209 to what has no Longshot PR yet, drop bd2b56d3 and #156 carries it. Your call. - **3a04664c** (Hookshot #2896, native shader keys in draw statistics) is https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/169 (WIP, refreshed onto current main with a fixture that fails on the old source). Same question; I'll follow whichever you prefer. - **90bfb06a** (Hookshot #2732, dev_packs) is already merged as https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/147 (fdc2840b, with fixes from its review), so it can be dropped here.
Author
Contributor

Thanks for the careful review. I've split this PR as you suggested, so I'm closing it: every commit is accounted for
below, in the five PRs or as dropped.

  • #284 tools, checks and docs: restore/tools-checks-docs, 20 commits
  • #285 test-request pinning: restore/test-request-pins, 2 commits
  • #286 rc.4 player fixes: restore/rc4-player-fixes, 21 commits, test request T-DV9Q, claimable once a maintainer adds the testing wanted label
  • #287 renderer fixes: restore/renderer-fixes, 22 commits, test request T-TQY6, claimable once a maintainer adds the testing wanted label
  • #288 new defaults and speed-ups: restore/defaults-after-rc4, 26 commits, test request T-Y5MJ, claimable once a maintainer adds the testing wanted label

Each one is based on current main (514591a3), passes the game-free checks, and has been compiled for both platforms
(details in each PR). They are all WIP until they've had their runs in the game.

Must change

  1. Drop 90bfb06a (Hookshot #2732, scripts/dev_packs.py). Main already has it, as #147.

Done. It's in none of the split PRs, so main's newer scripts/dev_packs.py, tests/test_dev_packs.py, the
development.md section and the AGENTS.md line stay as #147 left them. One more commit went the same way:
4fc02edb (the floor web above 60 fps, https://github.com/NotRexed/Hookshot/pull/2929) reached main as #49 after this
PR opened, with that version's comments, so it's dropped too.

  1. Rebase onto current main ... Re-run run_all.py after the rebase.

Done, twice. I first rebased this branch onto fbd42024 (#214, #197, #147, #78, #54 and the rest). #197's files merged
without conflicts. The front cache's timers in build_descriptors conflicted with #107's handle tables; I kept #107's
code and put the timers inside it. run_all then found one break: #55's upscaler round-trip check cuts save(), which
https://github.com/NotRexed/Hookshot/pull/2966 makes call logf and lstat, and its fixture declared neither. That
fix is in #286. Main then moved to d9648863, and then to 514591a3 (#274, docs only), and the split branches are rebased onto that. Two more things came up:
Hookshot's Auto run option number, 55, is now main's Damage taken row (#138), so Auto run takes 56 in #288 (its commit
says so); and the draw statistics' StatsQuery (https://github.com/NotRexed/Hookshot/pull/2896) now sits beside #260's
OccGuard, with main's line kept, and #260's occlusion check names the new StatsQuery(cmd, p, d) call in
#288. Each split PR's run_all is in its Testing section: no failure other than
test_lift_cache, which fails on main the same way here.

  1. Build the branch for Windows and Linux before it goes out of WIP.

Partly done. I found tools.lock's llvm-mingw on this PC, so the Windows target was cross-built here, and Linux was built
with clang and lld. I have no game build here, so this covers what can be built without one. For each split branch, and
for all five merged together, nx2/build.py --standalone with an empty lift compiled every source in the repository that
the game links (the host, its libraries and the Vulkan back end: about 400 files per platform) with no failure. The link
stopped only at the symbols the game's lifted code provides, the same list as on main. RX_VK_ONLY linked
longshot_vk.so and longshot_vk.dll, and builder/setup/setup.c linked as LongshotBuilder-Setup.exe. What's still
missing is a full game build on each platform and a short run in the game. Those are open as test requests: T-DV9Q, claimable once a maintainer adds the testing wanted label
for #286, T-TQY6, claimable once a maintainer adds the testing wanted label for #287 and T-Y5MJ, claimable once a maintainer adds the testing wanted label for #288. The PRs stay WIP until they're done.

Should change

  1. Split it before it merges.

Done, with your cut: a #284, b #285, c #286, d #287, e #288. The 11 tested-configuration rows and the README
form-feed fix are in a. My stand-in commit is split by what it stands in for (ViewFast in e, compile_all in d). Each
PR lists the Hookshot PRs it restores, with their authors, and asks for a rebase merge so each author keeps their
commit.

... each piece needs its own MKWVK_API_VERSION bump (22, then 23)

Done, in order. #286 (touchpad binding, https://github.com/NotRexed/Hookshot/pull/2989) takes 22. #287 (ultrawide
loading cover, https://github.com/NotRexed/Hookshot/pull/2939) takes 23 in a commit of its own, assuming c merges first.
Both descriptions say so, and say that if d merges first, it takes 22 and c takes 23. The Auto run option in e changes no
struct, so it doesn't bump.

  1. Each Hookshot reference needs its full URL.

Done. Every restored commit now has a Restored-from: https://github.com/NotRexed/Hookshot/pull/NNNN trailer. The
bare numbers in its message are full URLs, including Fixes #2180, #2182, #2220, #2579, #2954, #2885 and
#2667, so Forgejo won't close Longshot issues with those numbers. The (#NNNN) at the end of each title went into the
trailer, and other numbers in titles read "Hookshot PR NNNN" or "Hookshot issue NNNN". In the restored text, each PR
has a commit of mine that writes the bare Hookshot numbers it adds as URLs. That covers docs/test-requests.md's #2731,
the tested-configuration rows, tests/srccut.py, vk_memory.cpp, vk_texture.cpp, target_overlap.h and the
checks' docstrings, with the comments rewrapped. Longshot's own numbers (#55, #77, #107, #138, #171, #177, #181) are
left as they are. The descriptions use URLs too.

  1. The staging prewarm (Hookshot #2960) doesn't keep what it makes.

Left out, as you suggested. In #288 the commit is the texture-view front cache and the statistics only, retitled to
match, with a "Ported to Longshot" note saying why the prewarm half is gone. Keeping the arenas would need an exemption
on Arena and a check of its own, and that can be its own PR.

  1. Small description fixes

Done in the new descriptions. juliotec's fixes are eight, listed in #287. HOS_FS_OVERLAY isn't presented as new.
https://github.com/NotRexed/Hookshot/pull/2559's NVIDIA default is in #288, after rc.4, marked as needing a Longshot
measurement, with what it risks and its test request. The new switches with no entry in docs/development.md now have
one (d: RX_VK_TEMPORAL_CUT, RX_VK_SPLIT_UBFLUSH; e: seven).

Line comments

scripts/dev_packs.py: please drop 90bfb06a.

Dropped (point 1).

gpu/vk/vk_api.h: if the PR is split, whichever piece merges second needs its own bump to 23.

Done (point 4): c is 22 and d is 23, and both descriptions say what to do if they merge in the other order.

gpu/vk/vk_texture.cpp (the prewarm): exempt the prewarmed arenas, or leave this half of #2960 out for rc.4.

Left out (point 6).

gpu/vk/vk_texture.cpp (game_order): a new default for every NVIDIA player with no Longshot measurement.

Held out of the rc.4 PRs. It's in #288, after rc.4, and that description says it needs a measurement (main against the
PR in Kokiri Forest and Hyrule Field, RX_VK_GAME_ORDER=1 against =0, two interleaved runs each) and what it risks
if the game's barriers miss a hazard: flicker or wrong pixels, with RX_VK_GAME_ORDER=0 to turn it off. Test request
T-Y5MJ, claimable once a maintainer adds the testing wanted label.

docs/test-requests.md: the bare #2731 ... and the other Hookshot numbers.

Done (point 5): https://github.com/NotRexed/Hookshot/pull/2731, in #285, and the rest in each PR.

Not checked

I didn't review the items on your not-checked list beyond building them and running the checks. The pipeline-list
binaries in e are as they merged upstream.

Every commit of #209 and where it went (82: 81 restored and mine)
#209 commit Hookshot Title Now in
53000848 2949 fix(agents): a mistyped date in a claim does not stop the test request replay #284
10d978eb 2616 docs: tested configurations: RX 7900 XTX, RADV Mesa 26.2.6, Ubuntu 24.04 GNOME X11 (v2.1.0... #284
ee2692b6 2778 docs(tested): record paired Linux RTX 4090 rolling results #284
033d0dab 2771 docs: add tested-configurations rows for DLSS Frame Generation on an RTX 4090 (Windows) #284
39fef1de 2844 docs(tested): RTX 5080 on Windows, Hookshot 2306's native float add against the loop #284
e83a8948 2757 docs: add the tested-configurations row for Hookshot 2748's game run #284
e3211468 2912 docs: add tested-configurations rows for the recorder thread on an RTX 4090 (Windows) #284
e2a4b9a6 3014 docs(tested): RX 7700 XT on CachyOS, v2.2.0-rc.1 in Kokiri Forest #284
ec7d65c7 3028 docs: tested configurations: RX 5500 XT 8 GB (RDNA1, no mesh shaders), Windows 11, v2.2.0-... #284
b77f804d 2715 docs: tested configurations: RX 5600 XT 6 GB (RDNA1, no mesh shaders), Windows 11, main de... #284
40f423ed 2889 docs: add RTX 5050 Laptop 8 GB runs with and without RX_VK_COMPACT=1 on v2.1.0 #284
0d380a28 2840 docs(tested): RTX 5090 on Windows, Kokiri Forest: main against v2.1.0, 49f75dc8 and the la... #284
b33e13df 3021 fix(setup): a rename blocked by a scanner is tried again, and the message carries Windows'... #286
641bba35 3029 build(release): a shipped pipeline list from a player's progress on Linux AMD, 1364 pipeli... #288
d2ca5830 3030 build(release): a shipped pipeline list from an overnight tour of 21 areas, 3388 pipelines #288
fc649e08 2877 test(vk): execute startup pool placement and fallback checks #284
e1927326 2887 feat(tests): run_all.py --changed runs the checks that read what changed #284
ce9e6504 2553 fix: readme's work/ffx-sdk, and the build log names an FSR 4 tick without AMD's license #284
f424c820 2703 build(release): the RTX 5090 fork list's note says what its build changed (Hookshot 2143 f... #284
52752fcd 2714 feat(scripts): hookshot_bench.py, a repeatable benchmark route through the control bridge #284
90bfb06a 2732 feat(dev): test a change against a base, as a pack of what it changes dropped: main has the newer #147
e7922e44 2830 feat(agents): pin test requests and results to commits #285
7e8e5b06 2866 ci: execute authored Vulkan fixtures on lavapipe #284
ec01d41e 2914 test: one source cutter for the checks (tests/srccut.py), 80 checks moved to it #284
8a829543 3017 fix(memory): a failed allocation at the commit limit no longer closes the game (Hookshot 2... #286
4fc02edb 2929 fix(runtime): let the floor web react above 60 fps dropped: main has it as #49 (merged since this PR opened)
b67d539e 2933 fix(menu): a control's second key survives a rebind, with a column of its own #286
288cb744 2944 fix(input): custom ocarina note buttons count with the controller in the background #286
59c9587f 2947 fix(input): the native mouse camera leaves the view to the game while the mouse is release... #286
4160ef2b 2948 fix(menu): a bug report whose report.txt could not be written out is not shown as saved #286
68aea4a5 2952 fix(fsr4): the tail is written before its reader also when the unrolled shader arrives bet... #287
abd42c6e 2961 fix(input): Mouse 4 and Mouse 5 can be bound on Linux #286
fd061e48 2962 fix(input): the gyro is at rest while the Insert menu is open #286
c9a08894 2966 fix(menu): a save that fails leaves the options file as it was #286
1575198f 2970 fix(linux): the sound output is closed before it is opened again #286
e8391dc5 2978 fix(menu): Windows' reason for a bug report that could not be saved is UTF-8 whatever the ... #286
688ae129 2982 fix(vk): a background-prepared update makes the state list's pipelines behind the preparin... #287
74baebcf 2983 fix(vk): a half-written last record is cut off before the pipeline lists are added to #287
41c2482f 2985 fix(builder): a preparation the GPU driver stalled is not promised for the first start #286
8bcab34f 2987 fix(vk): the networks a card leaves out are not logged or counted as shader failures #287
839af6ec 2989 feat(input): a DualShock 4 or DualSense touchpad click can be bound in Controls #286
6a7f3123 3002 fix(vk): a GPU buffer write into a texture's storage is found under a later, smaller write #287
f4e8fc27 3003 fix(vk): a texture whose upload failed is tried again, not left empty or stale #287
140480be 3018 fix(oot): the scarecrow's recall of the recorded song takes the note remaps too (kind 11) #286
1238a5e2 2953 fix(build): a direct lift removes an earlier lift's split parts before it writes its own #286
ba93d640 2968 fix(builder): a refused extracted dump leaves the saved copy of the game code alone #286
1fbf8fa4 2747 feat(input): Auto run, the sprint goes on after A is let go #288 (option 56: #138 took 55)
75f31e38 2614 feat(vk): FSR 3.1 on the chain keeps the game's native HDR highlights #287
da3a67f7 2625 fix(vk): one pipeline play file a session, and the next start puts them together #288
0d1c0178 2939 fix(ultrawide): the sides stay closed while the loading cover shows in the middle 16:9 #287
9d5f5a43 2897 fix(vk): report GPU resource waits beyond recording-slot reuse #288
a4a52e42 2903 fix(linux): enable ReplayPin on the reserved core and reset child thread affinity #288
0c5b3c01 2782 fix(shaders): the noperspective workaround baked offline too; RX_VK_LOWER_SKIP in the prog... #288
2193b94b 3044 fix(audio): Auto counts 5.1 speakers, not channels #286
9bc54962 2554 perf(vk): opaque ASTC textures kept as BC1, half the video memory of BC3 #288
15f0c594 2557 feat(menu): a Texture detail row (Auto, Full, Medium, Low) for cards short of video memory #288
7612160d 2927 fix(vk): a camera cut starts a new upscaler history (DLSS, FSR, their frame generation) #287
a483172f 2875 fix(linux): the log says why the renderer did not load #286
aece4212 2559 perf(vk): back-to-back passes and dispatches ordered by the game's barriers (NVIDIA by def... #288
05c2c55c 3000 fix(vk): on Intel a split submission's uniform updates are written by its last part #287
d56f1f2f 2963 fix(input): the controller in the background stays out of the game while the Insert menu i... #286
3a04664c 2896 fix(vk): report native shader keys in draw statistics #288
7ea7b16b 2469 perf(vk): suballocate untouched tails of in-flight staging arenas #288
6d4565eb 2476 perf(vk): cache mesh shader picks in thread-local program caches #288
e7565a7f 2482 fix(vk): fall back to host pointers on udmabuf rejection and retry mirror allocation #288
bd2b56d3 2708 perf(host): three per-call costs on hot paths removed (own-thread lookups, a Signal with n... #288
5a997771 2766 fix(vk): let render targets go as newer ones together cover them, and reuse their images a... #288
79c1152e 2785 perf(vk): no pool import for one GPU unswizzle, and the CPU unswizzle threaded from 4 MB #288
1fa26f07 3043 perf(vk): a pool import does not pay a walk over the game's heap #288
b2e4cea9 2967 fix(vk): image pool blocks of 32 MB by default, not 128 #288
c20aeb64 3020 fix(vk): a large excess over the 90% rule or the driver's limit is cut to 7/8 again, only ... #288
e099e475 2960 fix(vk): keep the staging arenas, and answer repeated texture views from a front cache #288 (the view cache only; the staging prewarm left out)
49c34ebe 2976 fix: write 8/16-bit global stores directly instead of a compare-exchange loop #288
58500360 3031 fix(vk): bind a sparse texture's queued blocks when its upload runs out of staging #287
7fcb7a65 3032 fix(vk): do not reuse the read-back command buffer while a timed-out submission holds it #287
bfd36e7b 2691 fix(vk): validate texture blits before vkCmdBlitImage #287
2231454f 2718 fix(vk): skip clears of textures kept in ASTC blocks #287
a10e32a2 2719 fix(vk): keep mesh indirect argument ranges from overlapping #287
3d0e622b 2741 fix(vk): skip texel buffer views of formats the device cannot use #287
d6b1c6ca 2742 fix(vk): check the cooperative matrix probe's mapping, fence and timeout #287
998564b9 2743 fix(vk): retry sparse blocks whose bind failed #287
7bf35db4 - test: stand-ins for the texture views' front cache (Hookshot 2960) and Compile shaders (co... split: the ViewFast stand-ins went to #288, the compile_all one to #287
Thanks for the careful review. I've split this PR as you suggested, so I'm closing it: every commit is accounted for below, in the five PRs or as dropped. - [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) tools, checks and docs: `restore/tools-checks-docs`, 20 commits - [#285](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/285) test-request pinning: `restore/test-request-pins`, 2 commits - [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) rc.4 player fixes: `restore/rc4-player-fixes`, 21 commits, test request T-DV9Q, claimable once a maintainer adds the `testing wanted` label - [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) renderer fixes: `restore/renderer-fixes`, 22 commits, test request T-TQY6, claimable once a maintainer adds the `testing wanted` label - [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) new defaults and speed-ups: `restore/defaults-after-rc4`, 26 commits, test request T-Y5MJ, claimable once a maintainer adds the `testing wanted` label Each one is based on current `main` (`514591a3`), passes the game-free checks, and has been compiled for both platforms (details in each PR). They are all WIP until they've had their runs in the game. ## Must change > 1. Drop `90bfb06a` (Hookshot #2732, `scripts/dev_packs.py`). Main already has it, as #147. Done. It's in none of the split PRs, so main's newer `scripts/dev_packs.py`, `tests/test_dev_packs.py`, the development.md section and the AGENTS.md line stay as #147 left them. One more commit went the same way: `4fc02edb` (the floor web above 60 fps, https://github.com/NotRexed/Hookshot/pull/2929) reached main as #49 after this PR opened, with that version's comments, so it's dropped too. > 2. Rebase onto current `main` ... Re-run `run_all.py` after the rebase. Done, twice. I first rebased this branch onto `fbd42024` (#214, #197, #147, #78, #54 and the rest). #197's files merged without conflicts. The front cache's timers in `build_descriptors` conflicted with #107's handle tables; I kept #107's code and put the timers inside it. `run_all` then found one break: #55's upscaler round-trip check cuts `save()`, which https://github.com/NotRexed/Hookshot/pull/2966 makes call `logf` and `lstat`, and its fixture declared neither. That fix is in [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286). Main then moved to `d9648863`, and then to `514591a3` (#274, docs only), and the split branches are rebased onto that. Two more things came up: Hookshot's Auto run option number, 55, is now main's Damage taken row (#138), so Auto run takes 56 in [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) (its commit says so); and the draw statistics' `StatsQuery` (https://github.com/NotRexed/Hookshot/pull/2896) now sits beside #260's `OccGuard`, with main's line kept, and #260's occlusion check names the new `StatsQuery(cmd, p, d)` call in [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288). Each split PR's run_all is in its Testing section: no failure other than `test_lift_cache`, which fails on main the same way here. > 3. Build the branch for Windows and Linux before it goes out of WIP. Partly done. I found tools.lock's llvm-mingw on this PC, so the Windows target was cross-built here, and Linux was built with clang and lld. I have no game build here, so this covers what can be built without one. For each split branch, and for all five merged together, `nx2/build.py --standalone` with an empty lift compiled every source in the repository that the game links (the host, its libraries and the Vulkan back end: about 400 files per platform) with no failure. The link stopped only at the symbols the game's lifted code provides, the same list as on main. `RX_VK_ONLY` linked `longshot_vk.so` and `longshot_vk.dll`, and `builder/setup/setup.c` linked as `LongshotBuilder-Setup.exe`. What's still missing is a full game build on each platform and a short run in the game. Those are open as test requests: T-DV9Q, claimable once a maintainer adds the `testing wanted` label for [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286), T-TQY6, claimable once a maintainer adds the `testing wanted` label for [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) and T-Y5MJ, claimable once a maintainer adds the `testing wanted` label for [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288). The PRs stay WIP until they're done. ## Should change > 4. Split it before it merges. Done, with your cut: a [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284), b [#285](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/285), c [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286), d [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287), e [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288). The 11 tested-configuration rows and the README form-feed fix are in a. My stand-in commit is split by what it stands in for (`ViewFast` in e, `compile_all` in d). Each PR lists the Hookshot PRs it restores, with their authors, and asks for a rebase merge so each author keeps their commit. > ... each piece needs its own `MKWVK_API_VERSION` bump (22, then 23) Done, in order. [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) (touchpad binding, https://github.com/NotRexed/Hookshot/pull/2989) takes 22. [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) (ultrawide loading cover, https://github.com/NotRexed/Hookshot/pull/2939) takes 23 in a commit of its own, assuming c merges first. Both descriptions say so, and say that if d merges first, it takes 22 and c takes 23. The Auto run option in e changes no struct, so it doesn't bump. > 5. Each Hookshot reference needs its full URL. Done. Every restored commit now has a `Restored-from: https://github.com/NotRexed/Hookshot/pull/NNNN` trailer. The bare numbers in its message are full URLs, including `Fixes #2180`, `#2182`, `#2220`, `#2579`, `#2954`, `#2885` and `#2667`, so Forgejo won't close Longshot issues with those numbers. The `(#NNNN)` at the end of each title went into the trailer, and other numbers in titles read "Hookshot PR NNNN" or "Hookshot issue NNNN". In the restored text, each PR has a commit of mine that writes the bare Hookshot numbers it adds as URLs. That covers `docs/test-requests.md`'s #2731, the tested-configuration rows, `tests/srccut.py`, `vk_memory.cpp`, `vk_texture.cpp`, `target_overlap.h` and the checks' docstrings, with the comments rewrapped. Longshot's own numbers (#55, #77, #107, #138, #171, #177, #181) are left as they are. The descriptions use URLs too. > 6. The staging prewarm (Hookshot #2960) doesn't keep what it makes. Left out, as you suggested. In [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) the commit is the texture-view front cache and the statistics only, retitled to match, with a "Ported to Longshot" note saying why the prewarm half is gone. Keeping the arenas would need an exemption on `Arena` and a check of its own, and that can be its own PR. > 7. Small description fixes Done in the new descriptions. juliotec's fixes are eight, listed in [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287). `HOS_FS_OVERLAY` isn't presented as new. https://github.com/NotRexed/Hookshot/pull/2559's NVIDIA default is in [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288), after rc.4, marked as needing a Longshot measurement, with what it risks and its test request. The new switches with no entry in docs/development.md now have one (d: `RX_VK_TEMPORAL_CUT`, `RX_VK_SPLIT_UBFLUSH`; e: seven). ## Line comments > `scripts/dev_packs.py`: please drop `90bfb06a`. Dropped (point 1). > `gpu/vk/vk_api.h`: if the PR is split, whichever piece merges second needs its own bump to 23. Done (point 4): c is 22 and d is 23, and both descriptions say what to do if they merge in the other order. > `gpu/vk/vk_texture.cpp` (the prewarm): exempt the prewarmed arenas, or leave this half of #2960 out for rc.4. Left out (point 6). > `gpu/vk/vk_texture.cpp` (`game_order`): a new default for every NVIDIA player with no Longshot measurement. Held out of the rc.4 PRs. It's in [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288), after rc.4, and that description says it needs a measurement (main against the PR in Kokiri Forest and Hyrule Field, `RX_VK_GAME_ORDER=1` against `=0`, two interleaved runs each) and what it risks if the game's barriers miss a hazard: flicker or wrong pixels, with `RX_VK_GAME_ORDER=0` to turn it off. Test request T-Y5MJ, claimable once a maintainer adds the `testing wanted` label. > `docs/test-requests.md`: the bare `#2731` ... and the other Hookshot numbers. Done (point 5): https://github.com/NotRexed/Hookshot/pull/2731, in [#285](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/285), and the rest in each PR. ## Not checked I didn't review the items on your not-checked list beyond building them and running the checks. The pipeline-list binaries in e are as they merged upstream. <details><summary>Every commit of #209 and where it went (82: 81 restored and mine)</summary> | #209 commit | Hookshot | Title | Now in | |---|---|---|---| | `53000848` | [2949](https://github.com/NotRexed/Hookshot/pull/2949) | fix(agents): a mistyped date in a claim does not stop the test request replay | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `10d978eb` | [2616](https://github.com/NotRexed/Hookshot/pull/2616) | docs: tested configurations: RX 7900 XTX, RADV Mesa 26.2.6, Ubuntu 24.04 GNOME X11 (v2.1.0... | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `ee2692b6` | [2778](https://github.com/NotRexed/Hookshot/pull/2778) | docs(tested): record paired Linux RTX 4090 rolling results | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `033d0dab` | [2771](https://github.com/NotRexed/Hookshot/pull/2771) | docs: add tested-configurations rows for DLSS Frame Generation on an RTX 4090 (Windows) | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `39fef1de` | [2844](https://github.com/NotRexed/Hookshot/pull/2844) | docs(tested): RTX 5080 on Windows, Hookshot 2306's native float add against the loop | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `e83a8948` | [2757](https://github.com/NotRexed/Hookshot/pull/2757) | docs: add the tested-configurations row for Hookshot 2748's game run | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `e3211468` | [2912](https://github.com/NotRexed/Hookshot/pull/2912) | docs: add tested-configurations rows for the recorder thread on an RTX 4090 (Windows) | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `e2a4b9a6` | [3014](https://github.com/NotRexed/Hookshot/pull/3014) | docs(tested): RX 7700 XT on CachyOS, v2.2.0-rc.1 in Kokiri Forest | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `ec7d65c7` | [3028](https://github.com/NotRexed/Hookshot/pull/3028) | docs: tested configurations: RX 5500 XT 8 GB (RDNA1, no mesh shaders), Windows 11, v2.2.0-... | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `b77f804d` | [2715](https://github.com/NotRexed/Hookshot/pull/2715) | docs: tested configurations: RX 5600 XT 6 GB (RDNA1, no mesh shaders), Windows 11, main de... | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `40f423ed` | [2889](https://github.com/NotRexed/Hookshot/pull/2889) | docs: add RTX 5050 Laptop 8 GB runs with and without RX_VK_COMPACT=1 on v2.1.0 | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `0d380a28` | [2840](https://github.com/NotRexed/Hookshot/pull/2840) | docs(tested): RTX 5090 on Windows, Kokiri Forest: main against v2.1.0, 49f75dc8 and the la... | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `b33e13df` | [3021](https://github.com/NotRexed/Hookshot/pull/3021) | fix(setup): a rename blocked by a scanner is tried again, and the message carries Windows'... | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `641bba35` | [3029](https://github.com/NotRexed/Hookshot/pull/3029) | build(release): a shipped pipeline list from a player's progress on Linux AMD, 1364 pipeli... | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `d2ca5830` | [3030](https://github.com/NotRexed/Hookshot/pull/3030) | build(release): a shipped pipeline list from an overnight tour of 21 areas, 3388 pipelines | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `fc649e08` | [2877](https://github.com/NotRexed/Hookshot/pull/2877) | test(vk): execute startup pool placement and fallback checks | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `e1927326` | [2887](https://github.com/NotRexed/Hookshot/pull/2887) | feat(tests): run_all.py --changed runs the checks that read what changed | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `ce9e6504` | [2553](https://github.com/NotRexed/Hookshot/pull/2553) | fix: readme's work/ffx-sdk, and the build log names an FSR 4 tick without AMD's license | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `f424c820` | [2703](https://github.com/NotRexed/Hookshot/pull/2703) | build(release): the RTX 5090 fork list's note says what its build changed (Hookshot 2143 f... | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `52752fcd` | [2714](https://github.com/NotRexed/Hookshot/pull/2714) | feat(scripts): hookshot_bench.py, a repeatable benchmark route through the control bridge | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `90bfb06a` | [2732](https://github.com/NotRexed/Hookshot/pull/2732) | feat(dev): test a change against a base, as a pack of what it changes | **dropped**: main has the newer #147 | | `e7922e44` | [2830](https://github.com/NotRexed/Hookshot/pull/2830) | feat(agents): pin test requests and results to commits | [#285](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/285) | | `7e8e5b06` | [2866](https://github.com/NotRexed/Hookshot/pull/2866) | ci: execute authored Vulkan fixtures on lavapipe | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `ec01d41e` | [2914](https://github.com/NotRexed/Hookshot/pull/2914) | test: one source cutter for the checks (tests/srccut.py), 80 checks moved to it | [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284) | | `8a829543` | [3017](https://github.com/NotRexed/Hookshot/pull/3017) | fix(memory): a failed allocation at the commit limit no longer closes the game (Hookshot 2... | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `4fc02edb` | [2929](https://github.com/NotRexed/Hookshot/pull/2929) | fix(runtime): let the floor web react above 60 fps | **dropped**: main has it as #49 (merged since this PR opened) | | `b67d539e` | [2933](https://github.com/NotRexed/Hookshot/pull/2933) | fix(menu): a control's second key survives a rebind, with a column of its own | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `288cb744` | [2944](https://github.com/NotRexed/Hookshot/pull/2944) | fix(input): custom ocarina note buttons count with the controller in the background | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `59c9587f` | [2947](https://github.com/NotRexed/Hookshot/pull/2947) | fix(input): the native mouse camera leaves the view to the game while the mouse is release... | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `4160ef2b` | [2948](https://github.com/NotRexed/Hookshot/pull/2948) | fix(menu): a bug report whose report.txt could not be written out is not shown as saved | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `68aea4a5` | [2952](https://github.com/NotRexed/Hookshot/pull/2952) | fix(fsr4): the tail is written before its reader also when the unrolled shader arrives bet... | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `abd42c6e` | [2961](https://github.com/NotRexed/Hookshot/pull/2961) | fix(input): Mouse 4 and Mouse 5 can be bound on Linux | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `fd061e48` | [2962](https://github.com/NotRexed/Hookshot/pull/2962) | fix(input): the gyro is at rest while the Insert menu is open | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `c9a08894` | [2966](https://github.com/NotRexed/Hookshot/pull/2966) | fix(menu): a save that fails leaves the options file as it was | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `1575198f` | [2970](https://github.com/NotRexed/Hookshot/pull/2970) | fix(linux): the sound output is closed before it is opened again | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `e8391dc5` | [2978](https://github.com/NotRexed/Hookshot/pull/2978) | fix(menu): Windows' reason for a bug report that could not be saved is UTF-8 whatever the ... | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `688ae129` | [2982](https://github.com/NotRexed/Hookshot/pull/2982) | fix(vk): a background-prepared update makes the state list's pipelines behind the preparin... | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `74baebcf` | [2983](https://github.com/NotRexed/Hookshot/pull/2983) | fix(vk): a half-written last record is cut off before the pipeline lists are added to | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `41c2482f` | [2985](https://github.com/NotRexed/Hookshot/pull/2985) | fix(builder): a preparation the GPU driver stalled is not promised for the first start | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `8bcab34f` | [2987](https://github.com/NotRexed/Hookshot/pull/2987) | fix(vk): the networks a card leaves out are not logged or counted as shader failures | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `839af6ec` | [2989](https://github.com/NotRexed/Hookshot/pull/2989) | feat(input): a DualShock 4 or DualSense touchpad click can be bound in Controls | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `6a7f3123` | [3002](https://github.com/NotRexed/Hookshot/pull/3002) | fix(vk): a GPU buffer write into a texture's storage is found under a later, smaller write | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `f4e8fc27` | [3003](https://github.com/NotRexed/Hookshot/pull/3003) | fix(vk): a texture whose upload failed is tried again, not left empty or stale | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `140480be` | [3018](https://github.com/NotRexed/Hookshot/pull/3018) | fix(oot): the scarecrow's recall of the recorded song takes the note remaps too (kind 11) | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `1238a5e2` | [2953](https://github.com/NotRexed/Hookshot/pull/2953) | fix(build): a direct lift removes an earlier lift's split parts before it writes its own | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `ba93d640` | [2968](https://github.com/NotRexed/Hookshot/pull/2968) | fix(builder): a refused extracted dump leaves the saved copy of the game code alone | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `1fbf8fa4` | [2747](https://github.com/NotRexed/Hookshot/pull/2747) | feat(input): Auto run, the sprint goes on after A is let go | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) (option 56: #138 took 55) | | `75f31e38` | [2614](https://github.com/NotRexed/Hookshot/pull/2614) | feat(vk): FSR 3.1 on the chain keeps the game's native HDR highlights | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `da3a67f7` | [2625](https://github.com/NotRexed/Hookshot/pull/2625) | fix(vk): one pipeline play file a session, and the next start puts them together | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `0d1c0178` | [2939](https://github.com/NotRexed/Hookshot/pull/2939) | fix(ultrawide): the sides stay closed while the loading cover shows in the middle 16:9 | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `9d5f5a43` | [2897](https://github.com/NotRexed/Hookshot/pull/2897) | fix(vk): report GPU resource waits beyond recording-slot reuse | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `a4a52e42` | [2903](https://github.com/NotRexed/Hookshot/pull/2903) | fix(linux): enable ReplayPin on the reserved core and reset child thread affinity | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `0c5b3c01` | [2782](https://github.com/NotRexed/Hookshot/pull/2782) | fix(shaders): the noperspective workaround baked offline too; RX_VK_LOWER_SKIP in the prog... | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `2193b94b` | [3044](https://github.com/NotRexed/Hookshot/pull/3044) | fix(audio): Auto counts 5.1 speakers, not channels | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `9bc54962` | [2554](https://github.com/NotRexed/Hookshot/pull/2554) | perf(vk): opaque ASTC textures kept as BC1, half the video memory of BC3 | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `15f0c594` | [2557](https://github.com/NotRexed/Hookshot/pull/2557) | feat(menu): a Texture detail row (Auto, Full, Medium, Low) for cards short of video memory | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `7612160d` | [2927](https://github.com/NotRexed/Hookshot/pull/2927) | fix(vk): a camera cut starts a new upscaler history (DLSS, FSR, their frame generation) | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `a483172f` | [2875](https://github.com/NotRexed/Hookshot/pull/2875) | fix(linux): the log says why the renderer did not load | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `aece4212` | [2559](https://github.com/NotRexed/Hookshot/pull/2559) | perf(vk): back-to-back passes and dispatches ordered by the game's barriers (NVIDIA by def... | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `05c2c55c` | [3000](https://github.com/NotRexed/Hookshot/pull/3000) | fix(vk): on Intel a split submission's uniform updates are written by its last part | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `d56f1f2f` | [2963](https://github.com/NotRexed/Hookshot/pull/2963) | fix(input): the controller in the background stays out of the game while the Insert menu i... | [#286](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/286) | | `3a04664c` | [2896](https://github.com/NotRexed/Hookshot/pull/2896) | fix(vk): report native shader keys in draw statistics | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `7ea7b16b` | [2469](https://github.com/NotRexed/Hookshot/pull/2469) | perf(vk): suballocate untouched tails of in-flight staging arenas | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `6d4565eb` | [2476](https://github.com/NotRexed/Hookshot/pull/2476) | perf(vk): cache mesh shader picks in thread-local program caches | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `e7565a7f` | [2482](https://github.com/NotRexed/Hookshot/pull/2482) | fix(vk): fall back to host pointers on udmabuf rejection and retry mirror allocation | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `bd2b56d3` | [2708](https://github.com/NotRexed/Hookshot/pull/2708) | perf(host): three per-call costs on hot paths removed (own-thread lookups, a Signal with n... | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `5a997771` | [2766](https://github.com/NotRexed/Hookshot/pull/2766) | fix(vk): let render targets go as newer ones together cover them, and reuse their images a... | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `79c1152e` | [2785](https://github.com/NotRexed/Hookshot/pull/2785) | perf(vk): no pool import for one GPU unswizzle, and the CPU unswizzle threaded from 4 MB | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `1fa26f07` | [3043](https://github.com/NotRexed/Hookshot/pull/3043) | perf(vk): a pool import does not pay a walk over the game's heap | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `b2e4cea9` | [2967](https://github.com/NotRexed/Hookshot/pull/2967) | fix(vk): image pool blocks of 32 MB by default, not 128 | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `c20aeb64` | [3020](https://github.com/NotRexed/Hookshot/pull/3020) | fix(vk): a large excess over the 90% rule or the driver's limit is cut to 7/8 again, only ... | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `e099e475` | [2960](https://github.com/NotRexed/Hookshot/pull/2960) | fix(vk): keep the staging arenas, and answer repeated texture views from a front cache | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) (the view cache only; the staging prewarm left out) | | `49c34ebe` | [2976](https://github.com/NotRexed/Hookshot/pull/2976) | fix: write 8/16-bit global stores directly instead of a compare-exchange loop | [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288) | | `58500360` | [3031](https://github.com/NotRexed/Hookshot/pull/3031) | fix(vk): bind a sparse texture's queued blocks when its upload runs out of staging | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `7fcb7a65` | [3032](https://github.com/NotRexed/Hookshot/pull/3032) | fix(vk): do not reuse the read-back command buffer while a timed-out submission holds it | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `bfd36e7b` | [2691](https://github.com/NotRexed/Hookshot/pull/2691) | fix(vk): validate texture blits before vkCmdBlitImage | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `2231454f` | [2718](https://github.com/NotRexed/Hookshot/pull/2718) | fix(vk): skip clears of textures kept in ASTC blocks | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `a10e32a2` | [2719](https://github.com/NotRexed/Hookshot/pull/2719) | fix(vk): keep mesh indirect argument ranges from overlapping | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `3d0e622b` | [2741](https://github.com/NotRexed/Hookshot/pull/2741) | fix(vk): skip texel buffer views of formats the device cannot use | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `d6b1c6ca` | [2742](https://github.com/NotRexed/Hookshot/pull/2742) | fix(vk): check the cooperative matrix probe's mapping, fence and timeout | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `998564b9` | [2743](https://github.com/NotRexed/Hookshot/pull/2743) | fix(vk): retry sparse blocks whose bind failed | [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | | `7bf35db4` | - | test: stand-ins for the texture views' front cache (Hookshot 2960) and Compile shaders (co... | split: the `ViewFast` stand-ins went to [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288), the `compile_all` one to [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287) | </details>
TrashPanda closed this pull request 2026-11-13 19:22:21 +00:00
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Vulkan fixtures (lavapipe, validation) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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!209
No description provided.