feat(vk): load shader programs when the game creates them #2760

Closed
github-sync wants to merge 15 commits from github/pr/2760 into main
Owner

@hckrman101 opened this pull request on GitHub · 2026-11-12 11:37 UTC · original
Merged on GitHub in 86a9cda235

On by default where the GPU has mesh shaders; today's behaviour elsewhere. RX_VK_LOAD=start or create overrides. Known limits are listed under "Not tested" and "Follow-ups".

Today every listed program and pipeline is made ready before the title, whatever the session goes on to draw: about 18,000 programs and 25,000 pipelines, 4 seconds and close to 8 GB of host heap on an RTX 3090. This loads a program's stored variants and its listed pipelines when the game creates the program, on background workers, from the caches only. Everything is still compiled in the prepare step; nothing is translated or compiled in play.

What is in it

  1. Compute programs in the shipped pipeline list (two commits): the list can name compute programs, kept ready before the title, with a contributed list of 233 keys (keys and hashes only).

  2. One-variant compute load: a compute program's first dispatch reads the code of the variant it runs, not of every stored variant of its key. RX_VK_LOAD_ALL_VARIANTS=1 is the old way.

  3. Program life log and census (RX_VK_LOG_PROGRAM_LIFE=1) and a create call from the game to the renderer (renderer interface version 19 to 20).

  4. Refactor: one place makes a compute pipeline's create info and asks the caches for it.

  5. Load at create (RX_VK_LOAD=create|start): at start only an index is read (the notes by position; nothing big is loaded). At a create call a job reads the program's variants, makes modules and layouts, and asks the pipeline caches for its listed pipelines. A first draw or dispatch that arrives before its job runs the job itself or waits for it. Jobs publish only finished programs. Only programs a list names are loaded at create (RX_VK_LOAD_UNLISTED=1 loads the rest). On a GPU without mesh shaders the mesh-fallback programs and their listed pipelines are still made before the title.

  6. Bounded first start: the first start after a prepare builds each listed pipeline into the pipeline cache once and lets it go, then loads at create.

  7. Compute pipelines by module identifier (VK_EXT_shader_module_identifier): a compute variant seen before gets its pipeline at the create call without its code being read or a module made. Identifiers are learned into a side file as play goes, so existing folders need no new prepare for it.

  8. The default and a safety net:

    • Programs are loaded at their create calls by default where the GPU has mesh shaders. Without them (the mesh fallback) the start keeps today's behaviour: on the one such card measured, first uses waited seconds for loads (below).
    • In create mode the replay thread's time running or waiting for loads at first uses is counted. A session with one wait over 100 ms, or over 500 ms in all, leaves a marker and the next start of that install loads before the title again (a prepare removes the marker; RX_VK_LOAD=create forces). Measured where it works: 10-25 ms in all over a tour, longest 2-7 ms; where it does not: 2.5-4.7 s, longest 0.7-1.3 s.
    • A log line says which way was chosen and why.

Cost to players: one more prepare after updating (the renderer interface version and a struct size are part of the translator stamp); about 70 s on the test PC with a warm driver cache.

Measured: RTX 3090

Linux, NVIDIA 620.70, 2560x1440. Full tour of five area visits (Kokiri, Kakariko, Market, Field, Kokiri; walking and a pause menu in each; 11 minutes), from the same played-install state unless said. The game's own swap was 0 and memory pressure 0.16-0.39 % in every run. Every create run: 0 translations, 0 compiles, 0 draws or dispatches left out, 0 pipelines missing from a cache.

walking and pause phases frames over 33 / 50 / 100 ms heap in use, a minute in / at the end first uses needing work boot to play
start 34,736 27 / 15 / 1 7,892 / 8,119 MB 0 25 s
create (everything the game creates) 36,016 28 / 14 / 2 4,248 / 5,271 MB 12-17 20 s
create (same) 34,213 34 / 20 / 5 4,247 / 5,346 MB 12-17 20 s
start 35,138 32 / 21 / 6 7,898 / 8,130 MB 0 26 s
create, listed only (this PR's behaviour) 35,106 25 / 16 / 1 3,562 / 4,342 MB 12
create, listed only, new install 35,408 22 / 15 / 1 3,560 / 4,345 MB 83 (81 are compute first dispatches) 21 s
create, this head 34,408 43 / 18 / 5 3,560 / 4,408 MB 16 21 s
create, this head 34,778 34 / 18 / 5 3,560 / 4,380 MB 15
start, this head 35,293 28 / 15 / 1 7,892 / 8,130 MB 0
  • No frame over 33 ms in any of 11 tours is program or pipeline work. Tours differ by a 4096x4096 texture the game rewrites in some runs (16-171 ms each on the replay thread, in both modes: 2 of 3 start tours, 4 of 8 create tours); whether create mode makes it more likely is not settled. Without those frames: start 23-25 slow frames a tour, create 20-32.
  • First uses that still needed work on the replay thread in create mode: 9-22 ms in all over a tour, worst single 5.9 ms; in a traced run all were at the title, the save load or a teleport load.
  • Load jobs: about 9,590 a tour (6,175 listed only) on two low-priority workers, median 0.8 ms, 99th percentile 4.7-5.2 ms, worst 41-98 ms on a worker, holding no lock the replay thread needs.
  • Why it can work: 96.7 % of first uses came 100 ms or more after the program's create call (median 3.5 s), and 2,936 of 2,936 drew with the stages given at create. The game creates about 7 times more programs than it draws.
  • Flash check (Kakariko and Market, every frame captured): no flash that start mode does not also show; the strongest events sit at the same moments in both modes. Only the strongest events were compared (270 events in create, 245 in start).

Area changes (teleport to +30 s, three rounds; fps, then frames over 33 / 50 / 100 ms):

change start create, 2 workers (this PR)
Market 76.5 / 80.4 / 76.6 fps; 19/10/5, 17/10/7, 26/14/8 76.1 / 78.4 / 75.0 fps; 15/10/4, 13/9/4, 18/9/5
Field 84.0 / 78.7 / 71.4 fps; 4/2/0, 10/5/1, 19/10/3 76.1 / 80.7 / 78.5 fps; 8/3/0, 4/2/0, 5/1/0
Kakariko 69.5 / 67.0 / 41.1 fps; 11/5/3, 31/18/3, 122/58/14 70.8 / 66.9 / 69.3 fps; 13/8/2, 12/5/3, 15/7/4

Level with start here (the 41.1 fps start run had the highest memory pressure of the set). One worker and a throttle that parks loads during texture uploads showed no gain and are not in the PR. No slow upload in a change window overlapped a load job. On Windows (RTX 4070 SUPER, the first head of this PR, see the comment below) the Castle Town to field change was a little slower in create mode in both pairs (45-47 against 48 fps); that change has only 33-42 load jobs here, so it is not explained yet.

With no switch set, this head on the RTX 3090 logs "programs are loaded when the game makes them (create): the default where the GPU has mesh shaders"; 0 left out, 0 compiled, first-use loads 14.1 ms in all, no marker written.

First start after a prepare:

start mode create mode (bounded)
listed pipelines 25,337 + 1,119 compute kept, 12.8 s 25,337 built and let go, 11.3 s, 0 not made
peak RSS 12,816 MB 7,263 MB
heap at the title 7,743 MB 2,857 MB
boot to title 26 s 24 s

One-variant compute load on its own (two pairs, 144 programs, nothing kept ready): first-dispatch total 1,127 to 752 ms and 1,268 to 538 ms; worst 115 to 51 and 86 to 19 ms. Those four runs were on a swapping PC: the direction holds, the sizes are rough.

Measured: RX 580 (RADV, no mesh shaders, 16 GB PC)

Kokiri, Field, Dodongo's Cavern, Jabu-Jabu, Bottom of the Well. This PC is short of memory in both modes (several GB in swap), so times there are partly swap.

create create create start
draws and dispatches left out 0 0 0 0
translated / compiled in play 0 / 0 0 / 0 0 / 0 0 / 0
a first use ran / waited for a load 23 / 18 31 / 25 32 / 22 0 / 0
replay-thread time in those 3,089 ms 2,908 ms 4,733 ms 0
worst single 713 ms 711 ms 1,346 ms
load jobs, median / 99th percentile 7.5 / 122 ms 7.2 / 160 ms 7.5 / 139 ms
  • Correct, but first uses wait for loads there, which start mode does not. This is why a GPU without mesh shaders keeps start by default, and what the safety net is for. Neither compute identifiers (all 1,051 made that way) nor raising a worker's priority while a draw waits (tried, not in the PR) changed it: 2,557 ms, longest 884 ms. The long jobs are compute keys with 16-64 variants at the save's load; most likely scattered reads of a 7 GB file that PC cannot keep in memory (not proven).
  • A cold prepare in create mode completes there (2,000 s; start mode 2,016 s).
  • Bench on the same card, 159 compute pipelines: today's way about 1,000 ms (median 2.5-2.9 ms, 99th percentile 62 ms), almost all of it making the shader module; with VK_EXT_shader_module_identifier 47 ms and 0.20 MB read instead of 54.6 MB. On the RTX 3090 identifiers leave the driver's time unchanged (99th percentile about 4.5 ms) and halve the heap.
  • An earlier head left out 258 draws there: on a card without mesh shaders the start takes the mesh-fallback programs' records, so the loads queued at create built none of their listed pipelines. Fixed in the last commit (0 left out in three runs).

Tried and dropped

  • VK_KHR_pipeline_binary: 159 compute pipelines at 1.2-1.7 ms median against 0.4-0.6 with the pipeline cache, 2.2 MB a pipeline on NVIDIA.
  • Releasing programs when the game frees them: at most 8.5 % of the loaded units for over 2,000 reloads (the game frees and re-creates the same programs within a second).

Tests

New: test_program_life, test_program_load_wait, test_program_cache_one_variant, test_pipeline_state_computes, test_warm_bounded, test_module_ids, test_load_default. Full suite locally on Linux, one commit before the head: 470 passed; 3 fail here for missing local modules or an unrelated host test (CI passed them on the first push). CI passes on Windows and Linux at the previous head.

Not tested

  • Windows on this head. The first head was run on Windows 11 with an RTX 4070 SUPER by a tester (comment below): 3-5 GB lower peak working set, no shader work in play.
  • Intel. AMD only as above (RADV, one card).
  • A tour over areas the lists do not cover, with listed-only loading.
  • More than two workers in the game.
  • The RX 580's screenshots were not compared between modes; correctness there rests on the counts.

Follow-ups (not in this PR)

  • Loads at create on GPUs without mesh shaders and on PCs short of memory (the RX 580 waits above).
  • The Castle Town to field change on Windows; a run of this head on Windows.
  • VK_EXT_shader_module_identifier for graphics programs (on RADV making the module is most of a load: 159 compute pipelines 47 ms against about 1,000 ms).
  • A 16 GB PC with a mesh-shader card: a capped-memory pair is being run.

Touches the pipeline lookup, notes, precompile, draw and dispatch paths in vk_draw.cpp; #2649 changes the cache file read and save in the same file (different functions).

> [@hckrman101](https://github.com/hckrman101) opened this pull request on GitHub · 2026-11-12 11:37 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2760) > **Merged on GitHub** in https://git.magicbean.dev/MagicBeanDev/Hookshot/commit/86a9cda235046f32458776178d64b445861e22f6 **On by default where the GPU has mesh shaders; today's behaviour elsewhere. `RX_VK_LOAD=start` or `create` overrides.** Known limits are listed under "Not tested" and "Follow-ups". Today every listed program and pipeline is made ready before the title, whatever the session goes on to draw: about 18,000 programs and 25,000 pipelines, 4 seconds and close to 8 GB of host heap on an RTX 3090. This loads a program's stored variants and its listed pipelines when the game creates the program, on background workers, from the caches only. Everything is still compiled in the prepare step; nothing is translated or compiled in play. ## What is in it 1. **Compute programs in the shipped pipeline list** (two commits): the list can name compute programs, kept ready before the title, with a contributed list of 233 keys (keys and hashes only). 2. **One-variant compute load:** a compute program's first dispatch reads the code of the variant it runs, not of every stored variant of its key. `RX_VK_LOAD_ALL_VARIANTS=1` is the old way. 3. **Program life log and census** (`RX_VK_LOG_PROGRAM_LIFE=1`) and a create call from the game to the renderer (renderer interface version 19 to 20). 4. **Refactor:** one place makes a compute pipeline's create info and asks the caches for it. 5. **Load at create** (`RX_VK_LOAD=create|start`): at start only an index is read (the notes by position; nothing big is loaded). At a create call a job reads the program's variants, makes modules and layouts, and asks the pipeline caches for its listed pipelines. A first draw or dispatch that arrives before its job runs the job itself or waits for it. Jobs publish only finished programs. Only programs a list names are loaded at create (`RX_VK_LOAD_UNLISTED=1` loads the rest). On a GPU without mesh shaders the mesh-fallback programs and their listed pipelines are still made before the title. 6. **Bounded first start:** the first start after a prepare builds each listed pipeline into the pipeline cache once and lets it go, then loads at create. 7. **Compute pipelines by module identifier** (`VK_EXT_shader_module_identifier`): a compute variant seen before gets its pipeline at the create call without its code being read or a module made. Identifiers are learned into a side file as play goes, so existing folders need no new prepare for it. 8. **The default and a safety net:** - Programs are loaded at their create calls by default **where the GPU has mesh shaders**. Without them (the mesh fallback) the start keeps today's behaviour: on the one such card measured, first uses waited seconds for loads (below). - In create mode the replay thread's time running or waiting for loads at first uses is counted. A session with one wait over 100 ms, or over 500 ms in all, leaves a marker and the next start of that install loads before the title again (a prepare removes the marker; `RX_VK_LOAD=create` forces). Measured where it works: 10-25 ms in all over a tour, longest 2-7 ms; where it does not: 2.5-4.7 s, longest 0.7-1.3 s. - A log line says which way was chosen and why. **Cost to players: one more prepare after updating** (the renderer interface version and a struct size are part of the translator stamp); about 70 s on the test PC with a warm driver cache. ## Measured: RTX 3090 Linux, NVIDIA 620.70, 2560x1440. Full tour of five area visits (Kokiri, Kakariko, Market, Field, Kokiri; walking and a pause menu in each; 11 minutes), from the same played-install state unless said. The game's own swap was 0 and memory pressure 0.16-0.39 % in every run. Every create run: 0 translations, 0 compiles, 0 draws or dispatches left out, 0 pipelines missing from a cache. | walking and pause phases | frames | over 33 / 50 / 100 ms | heap in use, a minute in / at the end | first uses needing work | boot to play | |---|---|---|---|---|---| | start | 34,736 | 27 / 15 / 1 | 7,892 / 8,119 MB | 0 | 25 s | | create (everything the game creates) | 36,016 | 28 / 14 / 2 | 4,248 / 5,271 MB | 12-17 | 20 s | | create (same) | 34,213 | 34 / 20 / 5 | 4,247 / 5,346 MB | 12-17 | 20 s | | start | 35,138 | 32 / 21 / 6 | 7,898 / 8,130 MB | 0 | 26 s | | create, listed only (this PR's behaviour) | 35,106 | 25 / 16 / 1 | 3,562 / 4,342 MB | 12 | | | create, listed only, new install | 35,408 | 22 / 15 / 1 | 3,560 / 4,345 MB | 83 (81 are compute first dispatches) | 21 s | | create, this head | 34,408 | 43 / 18 / 5 | 3,560 / 4,408 MB | 16 | 21 s | | create, this head | 34,778 | 34 / 18 / 5 | 3,560 / 4,380 MB | 15 | | | start, this head | 35,293 | 28 / 15 / 1 | 7,892 / 8,130 MB | 0 | | - **No frame over 33 ms in any of 11 tours is program or pipeline work.** Tours differ by a 4096x4096 texture the game rewrites in some runs (16-171 ms each on the replay thread, in both modes: 2 of 3 start tours, 4 of 8 create tours); **whether create mode makes it more likely is not settled.** Without those frames: start 23-25 slow frames a tour, create 20-32. - First uses that still needed work on the replay thread in create mode: 9-22 ms in all over a tour, worst single 5.9 ms; in a traced run all were at the title, the save load or a teleport load. - Load jobs: about 9,590 a tour (6,175 listed only) on two low-priority workers, median 0.8 ms, 99th percentile 4.7-5.2 ms, worst 41-98 ms on a worker, holding no lock the replay thread needs. - Why it can work: 96.7 % of first uses came 100 ms or more after the program's create call (median 3.5 s), and 2,936 of 2,936 drew with the stages given at create. The game creates about 7 times more programs than it draws. - Flash check (Kakariko and Market, every frame captured): no flash that start mode does not also show; the strongest events sit at the same moments in both modes. Only the strongest events were compared (270 events in create, 245 in start). Area changes (teleport to +30 s, three rounds; fps, then frames over 33 / 50 / 100 ms): | change | start | create, 2 workers (this PR) | |---|---|---| | Market | 76.5 / 80.4 / 76.6 fps; 19/10/5, 17/10/7, 26/14/8 | 76.1 / 78.4 / 75.0 fps; 15/10/4, 13/9/4, 18/9/5 | | Field | 84.0 / 78.7 / 71.4 fps; 4/2/0, 10/5/1, 19/10/3 | 76.1 / 80.7 / 78.5 fps; 8/3/0, 4/2/0, 5/1/0 | | Kakariko | 69.5 / 67.0 / 41.1 fps; 11/5/3, 31/18/3, 122/58/14 | 70.8 / 66.9 / 69.3 fps; 13/8/2, 12/5/3, 15/7/4 | Level with start here (the 41.1 fps start run had the highest memory pressure of the set). One worker and a throttle that parks loads during texture uploads showed no gain and are not in the PR. No slow upload in a change window overlapped a load job. On Windows (RTX 4070 SUPER, the first head of this PR, see the comment below) the Castle Town to field change was a little slower in create mode in both pairs (45-47 against 48 fps); that change has only 33-42 load jobs here, so it is not explained yet. With no switch set, this head on the RTX 3090 logs "programs are loaded when the game makes them (create): the default where the GPU has mesh shaders"; 0 left out, 0 compiled, first-use loads 14.1 ms in all, no marker written. First start after a prepare: | | start mode | create mode (bounded) | |---|---|---| | listed pipelines | 25,337 + 1,119 compute kept, 12.8 s | 25,337 built and let go, 11.3 s, 0 not made | | peak RSS | 12,816 MB | 7,263 MB | | heap at the title | 7,743 MB | 2,857 MB | | boot to title | 26 s | 24 s | One-variant compute load on its own (two pairs, 144 programs, nothing kept ready): first-dispatch total 1,127 to 752 ms and 1,268 to 538 ms; worst 115 to 51 and 86 to 19 ms. Those four runs were on a swapping PC: the direction holds, the sizes are rough. ## Measured: RX 580 (RADV, no mesh shaders, 16 GB PC) Kokiri, Field, Dodongo's Cavern, Jabu-Jabu, Bottom of the Well. This PC is short of memory in both modes (several GB in swap), so times there are partly swap. | | create | create | create | start | |---|---|---|---|---| | draws and dispatches left out | 0 | 0 | 0 | 0 | | translated / compiled in play | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | | a first use ran / waited for a load | 23 / 18 | 31 / 25 | 32 / 22 | 0 / 0 | | replay-thread time in those | 3,089 ms | 2,908 ms | 4,733 ms | 0 | | worst single | 713 ms | 711 ms | 1,346 ms | | | load jobs, median / 99th percentile | 7.5 / 122 ms | 7.2 / 160 ms | 7.5 / 139 ms | | - **Correct, but first uses wait for loads there, which start mode does not. This is why a GPU without mesh shaders keeps `start` by default, and what the safety net is for.** Neither compute identifiers (all 1,051 made that way) nor raising a worker's priority while a draw waits (tried, not in the PR) changed it: 2,557 ms, longest 884 ms. The long jobs are compute keys with 16-64 variants at the save's load; most likely scattered reads of a 7 GB file that PC cannot keep in memory (not proven). - A cold prepare in create mode completes there (2,000 s; start mode 2,016 s). - Bench on the same card, 159 compute pipelines: today's way about 1,000 ms (median 2.5-2.9 ms, 99th percentile 62 ms), almost all of it making the shader module; with `VK_EXT_shader_module_identifier` 47 ms and 0.20 MB read instead of 54.6 MB. On the RTX 3090 identifiers leave the driver's time unchanged (99th percentile about 4.5 ms) and halve the heap. - An earlier head left out 258 draws there: on a card without mesh shaders the start takes the mesh-fallback programs' records, so the loads queued at create built none of their listed pipelines. Fixed in the last commit (0 left out in three runs). ## Tried and dropped - `VK_KHR_pipeline_binary`: 159 compute pipelines at 1.2-1.7 ms median against 0.4-0.6 with the pipeline cache, 2.2 MB a pipeline on NVIDIA. - Releasing programs when the game frees them: at most 8.5 % of the loaded units for over 2,000 reloads (the game frees and re-creates the same programs within a second). ## Tests New: `test_program_life`, `test_program_load_wait`, `test_program_cache_one_variant`, `test_pipeline_state_computes`, `test_warm_bounded`, `test_module_ids`, `test_load_default`. Full suite locally on Linux, one commit before the head: 470 passed; 3 fail here for missing local modules or an unrelated host test (CI passed them on the first push). CI passes on Windows and Linux at the previous head. ## Not tested - **Windows on this head.** The first head was run on Windows 11 with an RTX 4070 SUPER by a tester (comment below): 3-5 GB lower peak working set, no shader work in play. - **Intel.** AMD only as above (RADV, one card). - A tour over areas the lists do not cover, with listed-only loading. - More than two workers in the game. - The RX 580's screenshots were not compared between modes; correctness there rests on the counts. ## Follow-ups (not in this PR) - Loads at create on GPUs without mesh shaders and on PCs short of memory (the RX 580 waits above). - The Castle Town to field change on Windows; a run of this head on Windows. - `VK_EXT_shader_module_identifier` for graphics programs (on RADV making the module is most of a load: 159 compute pipelines 47 ms against about 1,000 ms). - A 16 GB PC with a mesh-shader card: a capped-memory pair is being run. Touches the pipeline lookup, notes, precompile, draw and dispatch paths in `vk_draw.cpp`; #2649 changes the cache file read and save in the same file (different functions). <!-- github-sync:item:2760 -->
A pipeline state list (HKSTATE2) can hold a record of kind 1 that names a compute
program by its compute stage's code key and nothing else; the record size and the
magic are unchanged, lists from before read as they did, and an older build drops
such a record. The game writes one at a program's first dispatch (and the learned
keys when the list is opened), the precompile keeps ready the shipped list's and
the learned ones, and the list a player sends carries them. A first start still
keeps every compute program. pipeline_states.py: add and --check take a sent list
only when it is exactly a list; check stops on a merge over COMPUTE_MOST.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
The compute programs that play on one PC dispatched from the title through Kokiri
Forest, Hyrule Field, Kakariko Village and the Market: their hashes only. The
pipeline tour's list holds none until the tour runs again.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
A compute key has up to 64 kept variants and a dispatch runs one. program_cache_load
now reads a compute key's variants without their code (the head and the stage's
descriptors, by position around the SPIR-V; asked of the disk together first on
Linux), the pick takes the variant it took before from the same list in the same
order, and vk_draw.cpp's dispatch has the one's code read and its module made
(program_cache_complete) before it uses the module. vk_shader.cpp is not touched,
so the translator stamp is the same and nothing is prepared again.
RX_VK_LOAD_ALL_VARIANTS=1: every variant whole, as before.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
The renderer first heard of a program at its first draw. The game's nvnProgramSetShaders now reaches it at once
(MkwVkRenderer::nvn_program_created, API version 20), and gpu/vk/vk_programs.cpp keeps each program's life: the create
call (its stages' code keys read from the game's memory then, whether the game is in a load), the free call, and the
first draw or dispatch with what it needed (a program read from hookshot_programs.bin, a translation, a pipeline from a
cache or compiled, left out). One census line every 600 presents sums it up, with what play made in all.

Log only: with the switch off the create call returns at once and a draw pays one compare.

vk_api.h and vk_shader.cpp are part of the translator stamp: every install prepares its shaders again once.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
The precompile, a first dispatch and the dispatch's own build each filled in the same VkComputePipelineCreateInfo, and two of
them asked the caches for it without compiling. compute_pipeline_info and compute_pipeline_cached are that, once. No change
in behaviour: RX_VK_NOOPT is still followed by play's builds and not by the precompile's.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Every listed program and pipeline was made ready before the title, whatever the session went on to draw: 17,081 programs
and 25,337 pipelines, 7.6 GB of heap, where a tour of five areas drew with about 3,300 programs. The game makes a program
(gives it its shaders) well before it first draws with it, so the work can be done then, for what the game makes.

RX_VK_LOAD=create (start, as before, is the default): nothing is kept ready by a list. A create call queues one background
job for the program's stages (vk_programs.cpp): its kept variants are read from hookshot_programs.bin by position (of a
key with several, those a note names), their modules and layouts made, and the pipelines the notes name asked of the
pipeline caches without compiling. A job never translates and never compiles; what it finds nowhere stays on the
first-draw path there was. A first draw that comes before its job ran runs it itself; one that comes while a worker runs
it waits. A job publishes only finished programs, and the pick checks them against the bound textures as before.

- the index is hookshot_pipelines.bin read by position (a note is 26 KB: none is read whole) and the keys the program
  cache holds; a create call tries the few vertex input and colour output type signatures the notes hold
- pipelines made ahead of their draw wait in a table of their own (the replay's map has no lock) and are asked of the
  start's cache first, so a session's play files do not hold them all again
- compute programs: every variant with what a pick needs and not its code; the variants play dispatched before
  (hookshot_compute_variants.bin, this install's own, hashes only) get their module and pipeline at the create call
- a light program's code place is in the Program itself, not in a table by its address
- the first start after a preparation always keeps everything ready, as start does: hookshot_pipelines.bin.built says a
  precompile that finished has built every note into the pipeline cache
- program_life.h: the states of what a program needs loaded, with the rules for letting one go (not used yet)
- PipelineSource: where a ready pipeline comes from, with the pipeline caches as its one implementation

sizeof(Program) is part of the translator stamp, as vk_api.h is (the commit before): one preparation again for every
install, for the two together.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Owner

@SadTransGirl commented on GitHub · 2026-11-12 12:30 UTC · original

Tested #2760 on Windows 11 (one of the gaps listed), RTX 4070 SUPER 12 GB, NVIDIA 617.42, Ryzen 9 9950X3D, 32 GB, DLAA 2560x1440, default settings, unattended, nothing else running. Build of the PR's head 8b975536; a warm-up start first (the first start after the prepare behaves as start), then RX_VK_LOAD=start against create, alternating, in one session. Peak working set from the process, frames from RX_VK_FRAME_TRACE.

run start create
Hyrule Field walk, 5 min: peak working set 20,120 / 20,739 MB 16,851 / 17,771 MB
the same: fps; frames over 33 / 50 / 100 ms 58.5, 78/33/12 · 58.8, 64/35/14 58.0, 91/54/24 · 58.4, 49/28/10
Castle Town to the field (an area change): peak working set 20,459 / 20,411 MB 17,485 / 18,681 MB
the same: fps; frames over 33 / 50 / 100 ms 48.1, 171/97/37 · 48.5, 135/75/35 46.9, 194/74/31 · 45.2, 279/80/31
Kokiri Forest, the camera turning (one run each): peak working set 18,960 MB 13,805 MB
the same: fps; frames over 33 / 50 / 100 ms 53.5, 159/53/13 57.5, 28/14/9
pipelines held at the end 25,538 7,164-8,996

In create mode: 0 translations, 0 compiles, 0 pipeline waits and 0 draws left out in every run; the census showed no first use needing work. Loads at create: 6,992-8,804 a run, median 0.60-0.79 ms, 99th percentile 3.8-5.9 ms, worst 31-68 ms, 1.4-1.9 GB read.

So on Windows: 3-5 GB less RAM at the peak, no shader work in play. Frame times are within the run-to-run spread in the walk. The area change was a little worse in create mode in both pairs (45.2-46.9 against 48.1-48.5 fps, more frames over 33 ms), so that may be worth a look: possibly the loads for the new area's programs competing with the area's own loading on two workers. Kokiri was better, but that's one pair. Not checked: flashes (no capture comparison), AMD/Intel.

> [@SadTransGirl](https://github.com/SadTransGirl) commented on GitHub · 2026-11-12 12:30 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2760#issuecomment-6823654775) Tested #2760 on Windows 11 (one of the gaps listed), RTX 4070 SUPER 12 GB, NVIDIA 617.42, Ryzen 9 9950X3D, 32 GB, DLAA 2560x1440, default settings, unattended, nothing else running. Build of the PR's head 8b975536; a warm-up start first (the first start after the prepare behaves as start), then `RX_VK_LOAD=start` against `create`, alternating, in one session. Peak working set from the process, frames from `RX_VK_FRAME_TRACE`. | run | start | create | |---|---|---| | Hyrule Field walk, 5 min: peak working set | 20,120 / 20,739 MB | 16,851 / 17,771 MB | | the same: fps; frames over 33 / 50 / 100 ms | 58.5, 78/33/12 · 58.8, 64/35/14 | 58.0, 91/54/24 · 58.4, 49/28/10 | | Castle Town to the field (an area change): peak working set | 20,459 / 20,411 MB | 17,485 / 18,681 MB | | the same: fps; frames over 33 / 50 / 100 ms | 48.1, 171/97/37 · 48.5, 135/75/35 | 46.9, 194/74/31 · 45.2, 279/80/31 | | Kokiri Forest, the camera turning (one run each): peak working set | 18,960 MB | 13,805 MB | | the same: fps; frames over 33 / 50 / 100 ms | 53.5, 159/53/13 | 57.5, 28/14/9 | | pipelines held at the end | 25,538 | 7,164-8,996 | In create mode: 0 translations, 0 compiles, 0 pipeline waits and 0 draws left out in every run; the census showed no first use needing work. Loads at create: 6,992-8,804 a run, median 0.60-0.79 ms, 99th percentile 3.8-5.9 ms, worst 31-68 ms, 1.4-1.9 GB read. So on Windows: 3-5 GB less RAM at the peak, no shader work in play. Frame times are within the run-to-run spread in the walk. The area change was a little worse in create mode in both pairs (45.2-46.9 against 48.1-48.5 fps, more frames over 33 ms), so that may be worth a look: possibly the loads for the new area's programs competing with the area's own loading on two workers. Kokiri was better, but that's one pair. Not checked: flashes (no capture comparison), AMD/Intel. <!-- github-sync:c:6823654775 -->
RX_VK_LOAD_UNLISTED=0 leaves programs with no noted pipeline to their first use; what was
loaded with no note is counted in the census. RX_VK_LOAD_WORKERS=0 is a measurement: every
load then runs at the first use. The load workers no longer take shader_mu: a finished unit
is marked loaded and the thread that first asks for the program publishes it, so a
below-normal worker can never hold the lock the presenting thread needs.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
When create mode finds no hookshot_pipelines.bin.built marker, the prepare step builds each
noted pipeline into the pipeline cache and lets it go at once, never keeping more than a
bounded batch alive, then writes the marker. It reports progress like the precompile and
runs only in the prepare/first start, never in play.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
The new program tests are compiled with -D_CRT_SECURE_NO_WARNINGS, as the neighbouring cache
tests and the game's own Windows build are: the MSVC C runtime marks tmpfile deprecated and
-Werror stopped program_cache_one_variant there. test_precompile_compute_guard reads the
precompile's compute arm by text: since the arm takes its create info and its cache answer
from compute_pipeline_info and compute_pipeline_cached, the fixture takes those two from the
source as well and arms the same identity check on the guarded call.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
RX_VK_LOAD_UNLISTED=0 becomes how RX_VK_LOAD=create behaves: a program no noted pipeline
names is loaded at its first use, as RX_VK_LOAD=start does. Measured over the full tour:
680 MB less host heap a minute into play and 980 MB less at the end, with no more first
uses needing work (12 against 15). RX_VK_LOAD_UNLISTED=1 loads them at create as before.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
On a GPU without mesh shaders the start keeps every program translated for the mesh
fallback ready, and takes its records as it reads them: the load a create call queues
then finds no record for that key and makes none of its noted pipelines. Their first
draws found the program ready and no pipeline (RX 580, RADV: 66 first draws left out and
43 taken from a cache on the replay thread, 273 draws of a tour; none with
RX_VK_LOAD=start, whose precompile builds them). The start now asks the caches for those
programs' noted pipelines itself, on the threads that make their compute pipelines, and
keeps them ready for the draw as a load would.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
VK_EXT_shader_module_identifier, where the driver has it (RX_VK_MODULE_IDS=0: off). A
compute program's module is learned by its identifier when it is first made from its
code, in hookshot_module_ids.bin beside the program cache (good for one program cache and
one driver's identifiers; a side file, so no stamped struct changes and no preparation is
asked again). In a later run the load at the program's create call makes a learned
variant's pipeline from the caches with no code read and no module made; where no cache
has the pipeline the code is read as before. The dispatch needs no module of a program
whose pipeline is there. An existing prepared folder has no identifiers: they are learned
lazily, in the first run that makes the modules.

The life log (RX_VK_LOG_PROGRAM_LIFE=1) now has every load job, not only those of 10 ms
or more, and each texture upload of over 15 ms on the same clock with the number of load
jobs running at its end.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
RX_VK_LOAD=create or start still says which. Without it: create where the GPU draws mesh
programs itself, start where it draws them through the compute fallback (there a start
keeps the fallback's programs ready, and on the one such GPU measured, an RX 580 with
16 GB, first uses waited 2.5-4.7 s over a tour for loads at create, the longest 0.7-1.3 s,
where start mode has none). The log says which and why.

A safety net for PCs where create does not work as measured: the replay thread's time
running or waiting for loads at first uses is counted, and a session with one wait over
100 ms or over 500 ms in all leaves hookshot_load_start.txt in the game folder, after
which the install starts with start again (measured where create works: 10-25 ms in all
over a tour, the longest 2-7 ms). A preparation, or deleting the file, removes it.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Co-Authored-By: Claude Fable 5.2 <[email protected]>
github-sync changed title from feat(vk): load shader programs when the game creates them (RX_VK_LOAD=create) to feat(vk): load shader programs when the game creates them 2026-11-12 17:58:59 +00:00
github-sync closed this pull request 2026-11-12 17:59:00 +00:00
Author
Owner

@mooki000 commented on GitHub · 2026-11-12 17:36 UTC · original

A heads-up now that this is in: on main, tests/test_program_cache_one_variant.py no longer compiles, so the Python source checks are red for every pull request that merges with main. Main's own runs keep being cancelled by the next merge, which is probably why no completed red run has shown it yet.

Checked on plain main f5c9acc1 (Linux, Clang 23.1; the same from 86a9cda2 on):

cache_functions.inc:242: error: use of undeclared identifier 'program_cache_payload'
cache_functions.inc:301: error: use of undeclared identifier 'disk_zstd'
14 errors generated.

Cause: two changes that each passed alone met on main. #1630 (136452cd, the zstd program cache) landed just before this one and added helpers and a global that program_cache_read and its neighbours now use; this pull request's fixture cuts those functions out of vk_progcache.cpp without them.

I have a fix for the fixture in work (it cuts the real new helpers in as it does the others, zstd off, no product change) and will open it within the hour, after its Windows run. If you would rather do it yourself, say so and I will drop mine.

The other red check on main, tests/test_readme_menu_rows.py (Skip opening has no README line), is what #2857 fixes.

> [@mooki000](https://github.com/mooki000) commented on GitHub · 2026-11-12 17:36 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2760#issuecomment-6831144099) A heads-up now that this is in: **on main, `tests/test_program_cache_one_variant.py` no longer compiles**, so the Python source checks are red for every pull request that merges with main. Main's own runs keep being cancelled by the next merge, which is probably why no completed red run has shown it yet. Checked on plain main `f5c9acc1` (Linux, Clang 23.1; the same from `86a9cda2` on): ``` cache_functions.inc:242: error: use of undeclared identifier 'program_cache_payload' cache_functions.inc:301: error: use of undeclared identifier 'disk_zstd' 14 errors generated. ``` Cause: two changes that each passed alone met on main. #1630 (`136452cd`, the zstd program cache) landed just before this one and added helpers and a global that `program_cache_read` and its neighbours now use; this pull request's fixture cuts those functions out of `vk_progcache.cpp` without them. I have a fix for the fixture in work (it cuts the real new helpers in as it does the others, zstd off, no product change) and will open it within the hour, after its Windows run. If you would rather do it yourself, say so and I will drop mine. The other red check on main, `tests/test_readme_menu_rows.py` (Skip opening has no README line), is what #2857 fixes. <!-- github-sync:c:6831144099 -->
Commenting is not possible because the repository is archived.
No description provided.