perf(vk): a program's descriptor counts are kept with it, and a build asks for the thread's writer once #41

Merged
HumanBean merged 4 commits from HumanBean/Longshot:perf/desc-counts into main 2026-11-13 05:16:33 +00:00
Owner

Why

Building a draw's descriptors is the largest single part of the replay's CPU time (about 2.1-2.9 ms a frame in Kokiri Forest). Two things in it were repeated for every draw without depending on the draw. Taking them out saves about 0.3 ms a frame of CPU time on the replay thread there; no frame-rate gain is claimed.

What changed

  • vk, the program's descriptor counts are kept with the program: build_descriptors (gpu/vk/vk_draw.cpp) walked six descriptor lists per stage before every build, only to size three reserve calls. The three numbers are the program's own: they are counted once and kept in Program::desc_counts (gpu/vk/vk_shader.h; one packed word, written only once the program is ready, since its stages are final then). RX_VK_DESC_COUNTS=0 counts them at every build, as before.
  • vk, the thread's descriptor writer is asked for once a build: dw() is a thread-local lookup, which in a library loaded with dlopen is a call into the loader; a build made about 26 of them. build_descriptors now takes the reference once.
  • Nothing else changes: the same writes in the same order, the same reserves.
  • tests: new tests/test_desc_counts.py: the packing compiled from the source (every count comes back, counts that do not fit are not kept), and the rules read from build_descriptors (kept only for a ready program, the switch, the reserve made from the counts).
  • tests: tests/test_mesh_fallback.py: its check of the writer's program reads the new spelling (out.prog = &p;).

Flags and settings

Flag New or changed What it does Default now Goal: on by default? Helps on Doesn't help on Untested on Untested, but should help on
RX_VK_DESC_COUNTS New 0 counts a program's descriptors at every build, as before, instead of keeping the counts with the program On (the counts are kept) After more testing: it still owes a flash check and a play session RTX 3090, Linux, Kokiri Forest: about 0.13-0.15 us a draw less in the descriptor phase Frame rate in that scene: the frame is bounded by the GPU there Windows, AMD, Intel, other areas than Kokiri Forest -

The switch is in the switch reference of docs/development.md ("The GPU" table).

Measurements

main: 15d4ae8c (2026-11-12)
this PR: perf/desc-counts @ 0fa31fa2
Hardware: RTX 3090, NVIDIA 620.70.06, Linux (CPU and RAM not recorded in this description)
Scene: Kokiri Forest, the pristine save, standing; 2048x1152 window, default options
Method: RX_VK_BENCH=3000,300 replays one frame 300 times on the replay thread and logs the phases; three runs a side. The frame differs a little between runs, so the figures are per draw
How to reproduce: start the game into Kokiri Forest on the pristine save with default options and RX_VK_BENCH=3000,300, stand still, and read the phases the benchmark logs

The timed build was on an older base than the branch head, and the numbers are not re-measured yet. Both sides were built on main 15d4ae8c: unchanged main, and this change on top of it. The change then moved onto a newer main as 0fa31fa2, and the branch head baee6044 merges the current main (94bbc205) into it and adds the switch's row to docs/development.md. No game was built or timed on the merged tree. How the merged tree differs in the files this PR touches (compared by diff after putting the project's new name in the old files): gpu/vk/vk_shader.h and the two tests differ in nothing else; gpu/vk/vk_draw.cpp also has main's change that lets the Descriptor reuse and Sampler cache rows apply in play, which adds one thread-local store to build_descriptors and is on both sides of any comparison with main. So the saving per draw should hold, but that is an expectation, not a measurement. A clean re-measure (unchanged main beside the change, at least three interleaved runs a side, medians, fps and frame time) is still owed: the PC these numbers come from is swapping heavily (about 24 GB in swap, and the last five timed runs on it were void for memory), so it waits for a PC that is not.

main @ 15d4ae8c this PR @ 0fa31fa2
descriptors / commands, runs 1 / 2 / 3 0.94 / 0.96 / 0.92 0.80 / 0.80 / 0.82
descriptors, us a draw, runs 1 / 2 / 3 0.916 / 0.904 / 1.229 0.760 / 0.838 / 0.921
programs, us a draw, runs 1 / 2 / 3 0.415 / 0.356 / 0.601 0.389 / 0.470 / 0.476
commands, us a draw, runs 1 / 2 / 3 0.972 / 0.941 / 1.340 0.954 / 1.043 / 1.117
draws in the replayed frame, runs 1 / 2 / 3 2,335 / 1,880 / 1,798 2,316 / 2,531 / 2,291

The PC was swapping during all six runs (other work on it), which moves every phase of a run together: run 3 on main (the PC swapping hard) is 30 % slower in every column. So the first row is the one to read, the descriptor phase against the commands phase of the same run: 0.92-0.96 on main, 0.80-0.83 with the change, in all three runs a side. That is about 0.13-0.15 us a draw, or 0.3 ms a frame of CPU time on the replay thread at 2,300 draws. The absolute figures of the two quiet runs (0.916 and 0.904 against 0.760) say the same. Programs and commands do not move, as expected of a change inside build_descriptors.

No frame-rate gain is claimed. In this scene the frame is bounded by the GPU's time and the present's wait for it, not by this thread's CPU time.

Not measured: fps (average and 1% low) and the median and p99 frame time; a median is not reported, the three runs of each side are. Whether the runs of the two sides were interleaved is not recorded in this description. The log lines themselves are not pasted here, only the figures read from them.

Testing

Tested:

  • tests/test_desc_counts.py (new) and tests/test_mesh_fallback.py, on Linux. Run again on the branch head baee6044 (the tree merged with main 94bbc205): both pass.
  • python -I -B tests/run_all.py: 501 passed, 2 failed, 26 skipped on this PC (Linux, 4 at once), on the tree of the earlier merge with main (e2a4d453); not run again on the merge with main 94bbc205, where only this PR's two tests were run. The two that fail need tools this PC lacks (zstandard, libtk) and fail on unchanged main here too.
  • In the game: only the benchmark runs above (six starts into Kokiri Forest on an RTX 3090, NVIDIA 620.70.06, Linux, the frame replayed 300 times each).
  • Checked but not in this PR: a wider version (the same once-a-function lookup in bind_descriptors, texture_view_of and the pass functions) measured no further gain in two runs.

Not tested:

  • A flash check and a play session: this PR still owes both, and stays work in progress until they are done. The game with this change was run only in the benchmark runs above; no flash check and no long session.
  • Other areas than Kokiri Forest; a session with programs still being translated (the counts are kept only once a program is ready).
  • A game built on the merged tree: the timed build was on the older main.

Needs other testers:

  • Windows: the game plays as before with the change, expecting no difference on screen (request to open)
  • AMD and Intel GPUs: the same check, expecting the same (request to open)

Screenshots

Nothing on screen.

Risk and rollback

The counts size the reserve calls of every draw's descriptor build, and the writer's reference is now taken once a build: a mistake would show as wrong or missing descriptors, on screen as flashing or missing draws, which is what the owed flash check is for. The counts are kept only once a program is ready. RX_VK_DESC_COUNTS=0 counts at every build as before (the writer's single lookup stays); reverting the commit removes both.

Reviewer notes

Read build_descriptors in gpu/vk/vk_draw.cpp first, then Program::desc_counts in gpu/vk/vk_shader.h. Work in progress: not for review until the flash check and the play session are in.

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. (By a merge, not a rebase: main 94bbc205 is merged into the branch head baee6044; the one conflict, in gpu/vk/vk_shader.h, was resolved in the earlier merge.)
  • 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). (Filled in; the flash check and the play session are owed, and the test requests are still to open.)
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (Not yet. Three runs a side on the same PC, scene and settings; but the timed build was on an older main than the branch head, the order of the runs is not recorded, and no median, fps or frame time was taken. A clean re-measure is waiting for a PC that is not swapping.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags table filled in; README / docs/development.md switch reference updated.
  • Screenshots for a change that shows on screen. (n/a)
  • A row in docs/tested-configurations.md for each measured run. (Not added.)
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed). (n/a)
  • The SPIR-V headers regenerated with glslang (a shader changed). (n/a)
  • A game-free test added for the fix, failing on main and passing here. (n/a: not a fix. A game-free test of the counts is added.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Windows not built.)
  • Docs updated. (The switch's row in docs/development.md.)
## Why Building a draw's descriptors is the largest single part of the replay's CPU time (about 2.1-2.9 ms a frame in Kokiri Forest). Two things in it were repeated for every draw without depending on the draw. Taking them out saves about 0.3 ms a frame of CPU time on the replay thread there; no frame-rate gain is claimed. ## What changed - vk, **the program's descriptor counts are kept with the program**: `build_descriptors` (`gpu/vk/vk_draw.cpp`) walked six descriptor lists per stage before every build, only to size three `reserve` calls. The three numbers are the program's own: they are counted once and kept in `Program::desc_counts` (`gpu/vk/vk_shader.h`; one packed word, written only once the program is ready, since its stages are final then). `RX_VK_DESC_COUNTS=0` counts them at every build, as before. - vk, **the thread's descriptor writer is asked for once a build**: `dw()` is a thread-local lookup, which in a library loaded with `dlopen` is a call into the loader; a build made about 26 of them. `build_descriptors` now takes the reference once. - Nothing else changes: the same writes in the same order, the same reserves. - tests: new `tests/test_desc_counts.py`: the packing compiled from the source (every count comes back, counts that do not fit are not kept), and the rules read from `build_descriptors` (kept only for a ready program, the switch, the reserve made from the counts). - tests: `tests/test_mesh_fallback.py`: its check of the writer's program reads the new spelling (`out.prog = &p;`). ## Flags and settings | Flag | New or changed | What it does | Default now | Goal: on by default? | Helps on | Doesn't help on | Untested on | Untested, but should help on | |---|---|---|---|---|---|---|---|---| | `RX_VK_DESC_COUNTS` | New | `0` counts a program's descriptors at every build, as before, instead of keeping the counts with the program | On (the counts are kept) | After more testing: it still owes a flash check and a play session | RTX 3090, Linux, Kokiri Forest: about 0.13-0.15 us a draw less in the descriptor phase | Frame rate in that scene: the frame is bounded by the GPU there | Windows, AMD, Intel, other areas than Kokiri Forest | - | The switch is in the switch reference of `docs/development.md` ("The GPU" table). ## Measurements main: 15d4ae8c (2026-11-12) this PR: perf/desc-counts @ 0fa31fa2 Hardware: RTX 3090, NVIDIA 620.70.06, Linux (CPU and RAM not recorded in this description) Scene: Kokiri Forest, the pristine save, standing; 2048x1152 window, default options Method: `RX_VK_BENCH=3000,300` replays one frame 300 times on the replay thread and logs the phases; three runs a side. The frame differs a little between runs, so the figures are per draw How to reproduce: start the game into Kokiri Forest on the pristine save with default options and `RX_VK_BENCH=3000,300`, stand still, and read the phases the benchmark logs **The timed build was on an older base than the branch head, and the numbers are not re-measured yet.** Both sides were built on main 15d4ae8c: unchanged main, and this change on top of it. The change then moved onto a newer main as 0fa31fa2, and the branch head baee6044 merges the current main (94bbc205) into it and adds the switch's row to `docs/development.md`. No game was built or timed on the merged tree. How the merged tree differs in the files this PR touches (compared by diff after putting the project's new name in the old files): `gpu/vk/vk_shader.h` and the two tests differ in nothing else; `gpu/vk/vk_draw.cpp` also has main's change that lets the Descriptor reuse and Sampler cache rows apply in play, which adds one thread-local store to `build_descriptors` and is on both sides of any comparison with main. So the saving per draw should hold, but that is an expectation, not a measurement. A clean re-measure (unchanged main beside the change, at least three interleaved runs a side, medians, fps and frame time) is still owed: the PC these numbers come from is swapping heavily (about 24 GB in swap, and the last five timed runs on it were void for memory), so it waits for a PC that is not. | | main @ 15d4ae8c | this PR @ 0fa31fa2 | |---|---|---| | descriptors / commands, runs 1 / 2 / 3 | 0.94 / 0.96 / 0.92 | 0.80 / 0.80 / 0.82 | | descriptors, us a draw, runs 1 / 2 / 3 | 0.916 / 0.904 / 1.229 | 0.760 / 0.838 / 0.921 | | programs, us a draw, runs 1 / 2 / 3 | 0.415 / 0.356 / 0.601 | 0.389 / 0.470 / 0.476 | | commands, us a draw, runs 1 / 2 / 3 | 0.972 / 0.941 / 1.340 | 0.954 / 1.043 / 1.117 | | draws in the replayed frame, runs 1 / 2 / 3 | 2,335 / 1,880 / 1,798 | 2,316 / 2,531 / 2,291 | The PC was swapping during all six runs (other work on it), which moves every phase of a run together: run 3 on main (the PC swapping hard) is 30 % slower in every column. So the first row is the one to read, the descriptor phase against the commands phase of the same run: 0.92-0.96 on main, 0.80-0.83 with the change, in all three runs a side. That is about 0.13-0.15 us a draw, or 0.3 ms a frame of CPU time on the replay thread at 2,300 draws. The absolute figures of the two quiet runs (0.916 and 0.904 against 0.760) say the same. Programs and commands do not move, as expected of a change inside `build_descriptors`. **No frame-rate gain is claimed.** In this scene the frame is bounded by the GPU's time and the present's wait for it, not by this thread's CPU time. Not measured: fps (average and 1% low) and the median and p99 frame time; a median is not reported, the three runs of each side are. Whether the runs of the two sides were interleaved is not recorded in this description. The log lines themselves are not pasted here, only the figures read from them. ## Testing **Tested:** - `tests/test_desc_counts.py` (new) and `tests/test_mesh_fallback.py`, on Linux. Run again on the branch head baee6044 (the tree merged with main 94bbc205): both pass. - `python -I -B tests/run_all.py`: 501 passed, 2 failed, 26 skipped on this PC (Linux, 4 at once), on the tree of the earlier merge with main (e2a4d453); not run again on the merge with main 94bbc205, where only this PR's two tests were run. The two that fail need tools this PC lacks (`zstandard`, `libtk`) and fail on unchanged main here too. - In the game: only the benchmark runs above (six starts into Kokiri Forest on an RTX 3090, NVIDIA 620.70.06, Linux, the frame replayed 300 times each). - Checked but not in this PR: a wider version (the same once-a-function lookup in `bind_descriptors`, `texture_view_of` and the pass functions) measured no further gain in two runs. **Not tested:** - **A flash check and a play session: this PR still owes both, and stays work in progress until they are done.** The game with this change was run only in the benchmark runs above; no flash check and no long session. - Other areas than Kokiri Forest; a session with programs still being translated (the counts are kept only once a program is ready). - A game built on the merged tree: the timed build was on the older main. **Needs other testers:** - Windows: the game plays as before with the change, expecting no difference on screen (request to open) - AMD and Intel GPUs: the same check, expecting the same (request to open) ## Screenshots Nothing on screen. ## Risk and rollback The counts size the `reserve` calls of every draw's descriptor build, and the writer's reference is now taken once a build: a mistake would show as wrong or missing descriptors, on screen as flashing or missing draws, which is what the owed flash check is for. The counts are kept only once a program is ready. `RX_VK_DESC_COUNTS=0` counts at every build as before (the writer's single lookup stays); reverting the commit removes both. ## Reviewer notes Read `build_descriptors` in `gpu/vk/vk_draw.cpp` first, then `Program::desc_counts` in `gpu/vk/vk_shader.h`. Work in progress: not for review until the flash check and the play session are in. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. (By a merge, not a rebase: main 94bbc205 is merged into the branch head baee6044; the one conflict, in `gpu/vk/vk_shader.h`, was resolved in the earlier merge.) - [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. - [ ] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). (Filled in; the flash check and the play session are owed, and the test requests are still to open.) - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (Not yet. Three runs a side on the same PC, scene and settings; but the timed build was on an older main than the branch head, the order of the runs is not recorded, and no median, fps or frame time was taken. A clean re-measure is waiting for a PC that is not swapping.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags table filled in; README / docs/development.md switch reference updated. - [ ] Screenshots for a change that shows on screen. (n/a) - [ ] A row in `docs/tested-configurations.md` for each measured run. (Not added.) - [ ] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). (n/a) - [ ] The SPIR-V headers regenerated with glslang (a shader changed). (n/a) - [ ] A game-free test added for the fix, failing on `main` and passing here. (n/a: not a fix. A game-free test of the counts is added.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Windows not built.) - [x] Docs updated. (The switch's row in `docs/development.md`.)
Building a draw's descriptors is the largest single part of the replay's CPU time, and two things in it did not
depend on the draw:

- build_descriptors walked six descriptor lists a stage before every build, only to size three reserves. The
  three counts are the program's own: counted once and kept in Program::desc_counts (one packed word, kept only
  once the program is ready, when its stages are final). RX_VK_DESC_COUNTS=0 counts them every time, as before.
- dw() is a thread-local lookup, a call into the loader in a library loaded with dlopen, and a build made about
  26 of them. The build takes the reference once.

The same writes in the same order, the same reserves.

Kokiri Forest, RTX 3090, Linux, one frame replayed 300 times (RX_VK_BENCH), three runs a side: the descriptor
phase goes from 0.92-0.96 to 0.80-0.83 of the commands phase's time in the same run, about 0.13-0.15 us a draw,
0.3 ms a frame at 2,300 draws. The other phases do not move.

tests/test_desc_counts.py checks the packing and the rules the build keeps; tests/test_mesh_fallback.py reads the
writer's new spelling.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Merge Longshot main into the descriptor counts change
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 2s
5357a067a9
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Owner

The description is now in the template's sections. No code changed and no new numbers: the same facts as before, with the commits the measurements were taken on named, and the checklist ticked only where true. It stays work in progress: the flash check and the play session are still owed.

The description is now in the template's sections. No code changed and no new numbers: the same facts as before, with the commits the measurements were taken on named, and the checklist ticked only where true. It stays work in progress: the flash check and the play session are still owed.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
docs(vk): RX_VK_DESC_COUNTS in the switch reference
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 2s
CI / Workflow lint (actionlint) (pull_request) Failing after 4s
Third-party parts / As their notes say (pull_request) Successful in 5s
PR description / Description follows the template (pull_request_target) Failing after 5s
baee6044cf
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Owner

Merged current main (94bbc205) into the branch (head baee6044) and added the RX_VK_DESC_COUNTS row to docs/development.md; the PR's two tests pass on the merged tree. No new numbers, and the flash check and play session are still owed: they and the re-measure are waiting for a PC that is not swapping, so this stays work in progress.

Merged current main (94bbc205) into the branch (head baee6044) and added the RX_VK_DESC_COUNTS row to docs/development.md; the PR's two tests pass on the merged tree. No new numbers, and the flash check and play session are still owed: they and the re-measure are waiting for a PC that is not swapping, so this stays work in progress.
HumanBean changed title from WIP: perf(vk): a program's descriptor counts are kept with it, and a build asks for the thread's writer once to perf(vk): a program's descriptor counts are kept with it, and a build asks for the thread's writer once 2026-11-13 05:16:28 +00:00
HumanBean deleted branch perf/desc-counts 2026-11-13 05:16:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!41
No description provided.