perf(vk): six small costs of a draw and of a submission's parts removed #391

Open
p0lux wants to merge 7 commits from p0lux/Longshot:perf/six-cpu-costs into main
Contributor

Why

On a CPU-bound machine the replay thread sets the frame rate, and it still does small things thousands of times a frame that buy nothing: a division at every uniform buffer bind, a call to ask whether the frame is being dumped at every texture, two walks over the pools at every part of a submission. Six of them go here: 2.7% fewer instructions and 3.7% fewer cycles a frame on a heavy view on a Steam Deck, with nothing drawn differently.

What changed

One commit each, in the order of the branch:

  • uniform buffers: uniform_range() in gpu/vk/vk_draw.cpp tested a bound offset with % by an alignment the compiler does not know (a 64-bit division), and built and destroyed a std::function for ub_overlay()'s allocator at every bind, about five a draw. Now a mask (the alignment is a power of two: Vulkan's, and stream_reserve() already refuses another), and ub_overlay() (gpu/vk/vk_replay.cpp, vk_draw.h) takes a plain function and its alignment.
  • a submission's parts (gpu/vk/vk_memory.cpp, at every memory_begin_submission(), one every 120 work ops): retired_imports_give_back() summed every live pool for a log line written every 1,200 parts and looked again at every retired pool whose import was long gone; the sum is made for the line only, and the oldest retired pools without an import are passed once. memory_rb_refresh() searched every readback page among the pools to copy it into a mirror; with no mirror made that is not freed (an integrated GPU, GCN, RX_VK_MIRROR=0) it returns, and still drops the pages not read for a while every 64th time.
  • the dump: dump_on() was return armed; in gpu/vk/vk_dump.cpp, a call from some 30 lines of vk_draw.cpp and vk_replay.cpp (twice a texture of every descriptor build); it is inline in vk_dump.h.
  • dynamic state: dyn_same() (vk_draw.cpp) compared 35 words and the raw bytes one at a time at every draw; two memcmp, the floats still by value.
  • logs that are off: draw() read use_counters.pipes_new, another file's thread-local, at every draw for the program life log; dispatch() formatted its program's key at every dispatch for RX_VK_LOG_PUSH. Only with the log on.
  • Linux: pools_mu (vk_memory.cpp) is an SRWLOCK, which off Windows is host/posix/windows.h's pthread_rwlock_t, dearer than a mutex for a lock only ever taken exclusive; there it is a std::mutex. Windows is unchanged.
  • checks: tests/parallel_rb_refresh_test.cpp (the mirrors' counts, and a case with no mirror), tests/test_same_dyn.py (a difference in the last word's last byte, -0 against 0), tests/dump_readback_test.cpp (the flag's name).
  • docs: a run file in docs/tested/.

Flags and settings

None.

Measurements

main: fbd42024 (2026-11-13)
this PR: perf/six-cpu-costs @ 6a62d31c (the six commits on that main). The branch is now those commits rebased onto main 0caf0618 (db2d4073 to c0598cbc) and a docs commit, with one line changed since the measurement: memory_rb_refresh() asks "no mirror made that is not freed" (st_mirrors == st_mirrors_freed) where it asked "no mirror bytes", because main's sparse mirrors (#250) can hold no byte yet. A Steam Deck has no mirrors: the path measured is the same.
Hardware: Steam Deck OLED (AMD Custom APU 0932, RADV VANGOGH, Mesa 26.2.0-devel), 16 GB, SteamOS 3.9.5, kernel 7.2.7-valve2; the GPU's clock fixed at 1600 MHz
Scene: three saves, standing still: a heavy view (about 2,150 draws a frame), Kokiri Forest (about 2,600), Hyrule Field (about 1,600); no screen (headless), handheld profile, Minimum preset, the game's own upscaler, no frame generation, no cap
Method: the back end built alone from each commit, on a host and game folder of main 000f19fb (vk_api.h is the same). Each round in the order main, main with the BC3 scratch's change of #384 (not this PR's), this PR; 3 rounds on the heavy view and 2 on the others. Each run starts the game, loads the save by script, waits 3 log windows (the first 360 presents in game: a window is the log's "N presents, X fps" line, every 120), then measures exactly 720 presents: fps over that span, the log's replay time, and the render thread's user-space cycles and instructions (perf stat -e cycles:u,instructions:u -t <the thread>) divided by 720. Medians on the heavy view, the mean of the two elsewhere
How to reproduce: a save on a view of 2,000 draws or more, perf stat -e cycles:u,instructions:u -t <the Presentation Thread's id> over a fixed count of presents with each build

main @ fbd42024 this PR @ 6a62d31c Δ
heavy view: instructions a frame 62.10 M 60.42 M -2.7%
heavy view: cycles a frame 75.32 M 72.54 M -3.7%
heavy view: replay 25.54 ms 25.30 ms -0.24 ms
heavy view: fps 24.34 24.39 +0.05
Kokiri Forest: instructions a frame 85.62 M 82.78 M -3.3%
Kokiri Forest: cycles a frame 89.28 M 86.08 M -3.6%
Kokiri Forest: replay 31.52 ms 30.14 ms -1.38 ms
Kokiri Forest: fps 22.94 22.73 -0.21
Hyrule Field: instructions a frame 44.03 M 43.85 M -0.4%
Hyrule Field: cycles a frame 50.83 M 50.37 M -0.9%
Hyrule Field: fps 21.30 21.27 -0.03
heavy view   instructions (M)  main 61.81 62.64 62.10 | this PR 59.85 60.42 60.54
             cycles (M)        main 75.32 76.88 74.27 | this PR 72.54 72.00 73.26
             replay (ms)       main 25.54 26.21 25.47 | this PR 25.06 25.30 25.30
             fps               main 24.34 24.27 24.35 | this PR 24.52 24.39 24.27
Kokiri       instructions (M)  main 83.96 87.28       | this PR 81.30 84.26
             cycles (M)        main 87.55 91.00       | this PR 84.71 87.45
             replay (ms)       main 30.79 32.24       | this PR 29.59 30.69
             fps               main 23.01 22.87       | this PR 22.69 22.77
Field        instructions (M)  main 43.94 44.12       | this PR 43.86 43.84
             cycles (M)        main 50.99 50.66       | this PR 50.09 50.65
             fps               main 21.26 21.33       | this PR 21.37 21.16

The measured commit is kept on the fork as measured/six-cpu-costs-6a62d31c (https://git.magicbean.dev/p0lux/Longshot/src/branch/measured/six-cpu-costs-6a62d31c).

The fps do not move on that main because the frame waits on read-backs there (about 41 ms a frame for 25 ms of replay). With that wait out of the way (RX_VK_READBACK_LATE=newest,3980, #238, on both sides) the same six commits, measured a few hours earlier together with the BC3 scratch's change of #384 (which does nothing to the CPU), 3 alternated pairs on the heavy view, medians:

main fbd42024 + #238 @ 3a6fbc0c the same + these six and #384's commit @ 22ed63d6 Δ
heavy view: fps 29.77 30.35 +0.58
heavy view: replay 29.80 ms 28.99 ms -0.81 ms
heavy view: cycles a frame 74.07 M 72.11 M -2.6%
heavy view: instructions a frame 61.49 M 60.04 M -2.4%
fps               29.93 29.77 29.10 | 30.35 30.37 30.11
replay (ms)       29.80 29.73 30.36 | 28.99 28.76 29.35
cycles (M)        74.07 74.03 75.47 | 72.11 72.00 72.60
instructions (M)  61.49 61.11 62.22 | 60.04 59.85 60.09

Testing

Tested:

  • python -I -B tests/run_all.py on macOS with the Vulkan headers given: 485 passed, 37 failed, 39 skipped; 36 of the 37 fail the same way on main 0caf0618 on that machine (no /proc, no posix_fallocate, Python 3.9, the Linux launch scripts and the like: nx2/test_nup_zstd_heads.py, and in tests/: test_appimage_package_retry, test_appimage_source_headers, test_bake_portability, test_chunk_split, test_collect_wide, test_control, test_frame_rate_cap_step, test_framegen_resources, test_game_layout, test_gpu_hang_watch, test_kept_translation, test_lift_cache, test_linux_preflight, test_longshot_launch, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children, test_mirror_trim, test_missing_nup, test_module_ids, test_movie_frame_count, test_oot_saves, test_oot_state_tap, test_pad_layout, test_pcache_header_read, test_pipeline_cache_file, test_pipeline_cache_save, test_pipeline_play_cache, test_prof_symbols, test_program_cache_cleanup, test_program_cache_one_variant, test_prompt_skip, test_sticky_hot_pieces, test_tested_configurations), and the last, test_rt_pool_minimum.py, failed in a run the machine took 316 s over and passes when run alone. test_parallel_rb_refresh.py, test_same_dyn.py, test_dump_readback.py, test_retired_give_back.py, test_mem_trim.py, test_ub_memo.py and test_ub_threads.py, which read this code, pass.
  • In the game, a Steam Deck OLED, without a screen: 7 runs of 6a62d31c on the three scenes and 3 more with pictures, and more than 80 runs of the same commits on top of other renderer-only changes: no error, no GPU timeout in the kernel log.
  • The picture, by the renderer's own captures (RX_VK_SHOTS), standing still on the three scenes: main twice, then this PR, the last capture both runs have compared pixel by pixel (the mean absolute difference of a channel, 0 to 255). Two runs of main differ by 18.3 to 18.9 (a scene starts in one of two states and never stands still); this PR against the two runs of main: 21.9 and 17.3 in Hyrule Field, 17.8 and 7.9 in Kokiri Forest, 15.2 and 21.8 on the heavy view: as main against itself.
  • With the picture, by a person: a build holding this change with other renderer-only ones (3a3a6475, on main 514591a3) played on the Steam Deck OLED on 2026-11-14: nothing wrong seen on screen.
  • Linux: the back end builds (clang, on the Deck). Windows: longshot_vk.dll cross-builds with llvm-mingw 20261104 from a branch holding these commits and two more renderer-only ones (#238's and #384's), 0 failed.

Not tested:

  • A PC whose pools have VRAM mirrors: memory_rb_refresh() takes its usual path there, which this does not change, but it was not run; the line changed after the measurement (above) is covered by tests/test_parallel_rb_refresh.py only.
  • Any hardware but a Steam Deck OLED; Windows in the game.

Needs other testers:

  • Windows, any card: a short play, expecting nothing drawn differently and no slower frame (request to open)

Screenshots

Nothing on screen.

Risk and rollback

Each commit removes work and keeps the result, and each can be reverted alone. The two that are not mechanical: memory_rb_refresh() returning early (it trusts two counters: st_mirrors moves where a pool's mstate becomes MIRROR_ON, st_mirrors_freed in mirror_free(); a mirror that went another way would only lose the shortcut, since the counts would then never be equal); and retired_imports_give_back() passing the retired pools whose import is gone (it relies on a retired pool never being imported again: Pool::memory is only set where a pool is made).

Reviewer notes

  • #231 (@dukenheimer) also edits uniform_range() (its c1 branch) and payload<T>(); I left payload<T>() out of this PR for that reason. The hunks do not overlap; whichever goes second rebases a few lines.
  • The back end is built one file at a time (no LTO), which is why dump_on() and the like are calls. An opt-in ThinLTO build measured about 1.5 to 2% fewer cycles on the same scenes on top of these; I can open that separately if it is wanted.

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No 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 file in docs/tested/ for each new measured run (format in docs/tested-configurations.md; no appended row or assembled view).
  • 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.
  • Docs updated.
## Why On a CPU-bound machine the replay thread sets the frame rate, and it still does small things thousands of times a frame that buy nothing: a division at every uniform buffer bind, a call to ask whether the frame is being dumped at every texture, two walks over the pools at every part of a submission. Six of them go here: 2.7% fewer instructions and 3.7% fewer cycles a frame on a heavy view on a Steam Deck, with nothing drawn differently. ## What changed One commit each, in the order of the branch: - uniform buffers: `uniform_range()` in `gpu/vk/vk_draw.cpp` tested a bound offset with `%` by an alignment the compiler does not know (a 64-bit division), and built and destroyed a `std::function` for `ub_overlay()`'s allocator at every bind, about five a draw. Now a mask (the alignment is a power of two: Vulkan's, and `stream_reserve()` already refuses another), and `ub_overlay()` (`gpu/vk/vk_replay.cpp`, `vk_draw.h`) takes a plain function and its alignment. - a submission's parts (`gpu/vk/vk_memory.cpp`, at every `memory_begin_submission()`, one every 120 work ops): `retired_imports_give_back()` summed every live pool for a log line written every 1,200 parts and looked again at every retired pool whose import was long gone; the sum is made for the line only, and the oldest retired pools without an import are passed once. `memory_rb_refresh()` searched every readback page among the pools to copy it into a mirror; with no mirror made that is not freed (an integrated GPU, GCN, `RX_VK_MIRROR=0`) it returns, and still drops the pages not read for a while every 64th time. - the dump: `dump_on()` was `return armed;` in `gpu/vk/vk_dump.cpp`, a call from some 30 lines of `vk_draw.cpp` and `vk_replay.cpp` (twice a texture of every descriptor build); it is inline in `vk_dump.h`. - dynamic state: `dyn_same()` (`vk_draw.cpp`) compared 35 words and the raw bytes one at a time at every draw; two `memcmp`, the floats still by value. - logs that are off: `draw()` read `use_counters.pipes_new`, another file's thread-local, at every draw for the program life log; `dispatch()` formatted its program's key at every dispatch for `RX_VK_LOG_PUSH`. Only with the log on. - Linux: `pools_mu` (`vk_memory.cpp`) is an SRWLOCK, which off Windows is `host/posix/windows.h`'s `pthread_rwlock_t`, dearer than a mutex for a lock only ever taken exclusive; there it is a `std::mutex`. Windows is unchanged. - checks: `tests/parallel_rb_refresh_test.cpp` (the mirrors' counts, and a case with no mirror), `tests/test_same_dyn.py` (a difference in the last word's last byte, -0 against 0), `tests/dump_readback_test.cpp` (the flag's name). - docs: a run file in `docs/tested/`. ## Flags and settings None. ## Measurements main: fbd42024 (2026-11-13) this PR: perf/six-cpu-costs @ 6a62d31c (the six commits on that main). The branch is now those commits rebased onto main 0caf0618 (db2d4073 to c0598cbc) and a docs commit, with one line changed since the measurement: `memory_rb_refresh()` asks "no mirror made that is not freed" (`st_mirrors == st_mirrors_freed`) where it asked "no mirror bytes", because main's sparse mirrors (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/250) can hold no byte yet. A Steam Deck has no mirrors: the path measured is the same. Hardware: Steam Deck OLED (AMD Custom APU 0932, RADV VANGOGH, Mesa 26.2.0-devel), 16 GB, SteamOS 3.9.5, kernel 7.2.7-valve2; the GPU's clock fixed at 1600 MHz Scene: three saves, standing still: a heavy view (about 2,150 draws a frame), Kokiri Forest (about 2,600), Hyrule Field (about 1,600); no screen (headless), handheld profile, Minimum preset, the game's own upscaler, no frame generation, no cap Method: the back end built alone from each commit, on a host and game folder of main 000f19fb (`vk_api.h` is the same). Each round in the order main, main with the BC3 scratch's change of https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/384 (not this PR's), this PR; 3 rounds on the heavy view and 2 on the others. Each run starts the game, loads the save by script, waits 3 log windows (the first 360 presents in game: a window is the log's "N presents, X fps" line, every 120), then measures exactly 720 presents: fps over that span, the log's replay time, and the render thread's user-space cycles and instructions (`perf stat -e cycles:u,instructions:u -t <the thread>`) divided by 720. Medians on the heavy view, the mean of the two elsewhere How to reproduce: a save on a view of 2,000 draws or more, `perf stat -e cycles:u,instructions:u -t <the Presentation Thread's id>` over a fixed count of presents with each build | | main @ fbd42024 | this PR @ 6a62d31c | Δ | |---|---|---|---| | heavy view: instructions a frame | 62.10 M | 60.42 M | -2.7% | | heavy view: cycles a frame | 75.32 M | 72.54 M | -3.7% | | heavy view: replay | 25.54 ms | 25.30 ms | -0.24 ms | | heavy view: fps | 24.34 | 24.39 | +0.05 | | Kokiri Forest: instructions a frame | 85.62 M | 82.78 M | -3.3% | | Kokiri Forest: cycles a frame | 89.28 M | 86.08 M | -3.6% | | Kokiri Forest: replay | 31.52 ms | 30.14 ms | -1.38 ms | | Kokiri Forest: fps | 22.94 | 22.73 | -0.21 | | Hyrule Field: instructions a frame | 44.03 M | 43.85 M | -0.4% | | Hyrule Field: cycles a frame | 50.83 M | 50.37 M | -0.9% | | Hyrule Field: fps | 21.30 | 21.27 | -0.03 | ``` heavy view instructions (M) main 61.81 62.64 62.10 | this PR 59.85 60.42 60.54 cycles (M) main 75.32 76.88 74.27 | this PR 72.54 72.00 73.26 replay (ms) main 25.54 26.21 25.47 | this PR 25.06 25.30 25.30 fps main 24.34 24.27 24.35 | this PR 24.52 24.39 24.27 Kokiri instructions (M) main 83.96 87.28 | this PR 81.30 84.26 cycles (M) main 87.55 91.00 | this PR 84.71 87.45 replay (ms) main 30.79 32.24 | this PR 29.59 30.69 fps main 23.01 22.87 | this PR 22.69 22.77 Field instructions (M) main 43.94 44.12 | this PR 43.86 43.84 cycles (M) main 50.99 50.66 | this PR 50.09 50.65 fps main 21.26 21.33 | this PR 21.37 21.16 ``` The measured commit is kept on the fork as `measured/six-cpu-costs-6a62d31c` (https://git.magicbean.dev/p0lux/Longshot/src/branch/measured/six-cpu-costs-6a62d31c). The fps do not move on that main because the frame waits on read-backs there (about 41 ms a frame for 25 ms of replay). With that wait out of the way (`RX_VK_READBACK_LATE=newest,3980`, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/238, on both sides) the same six commits, measured a few hours earlier together with the BC3 scratch's change of https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/384 (which does nothing to the CPU), 3 alternated pairs on the heavy view, medians: | | main fbd42024 + #238 @ 3a6fbc0c | the same + these six and #384's commit @ 22ed63d6 | Δ | |---|---|---|---| | heavy view: fps | 29.77 | 30.35 | +0.58 | | heavy view: replay | 29.80 ms | 28.99 ms | -0.81 ms | | heavy view: cycles a frame | 74.07 M | 72.11 M | -2.6% | | heavy view: instructions a frame | 61.49 M | 60.04 M | -2.4% | ``` fps 29.93 29.77 29.10 | 30.35 30.37 30.11 replay (ms) 29.80 29.73 30.36 | 28.99 28.76 29.35 cycles (M) 74.07 74.03 75.47 | 72.11 72.00 72.60 instructions (M) 61.49 61.11 62.22 | 60.04 59.85 60.09 ``` ## Testing **Tested:** - `python -I -B tests/run_all.py` on macOS with the Vulkan headers given: 485 passed, 37 failed, 39 skipped; 36 of the 37 fail the same way on main 0caf0618 on that machine (no `/proc`, no `posix_fallocate`, Python 3.9, the Linux launch scripts and the like: `nx2/test_nup_zstd_heads.py`, and in `tests/`: `test_appimage_package_retry`, `test_appimage_source_headers`, `test_bake_portability`, `test_chunk_split`, `test_collect_wide`, `test_control`, `test_frame_rate_cap_step`, `test_framegen_resources`, `test_game_layout`, `test_gpu_hang_watch`, `test_kept_translation`, `test_lift_cache`, `test_linux_preflight`, `test_longshot_launch`, `test_low_memory_notice`, `test_make_game_cache`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_missing_nup`, `test_module_ids`, `test_movie_frame_count`, `test_oot_saves`, `test_oot_state_tap`, `test_pad_layout`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`, `test_prompt_skip`, `test_sticky_hot_pieces`, `test_tested_configurations`), and the last, `test_rt_pool_minimum.py`, failed in a run the machine took 316 s over and passes when run alone. `test_parallel_rb_refresh.py`, `test_same_dyn.py`, `test_dump_readback.py`, `test_retired_give_back.py`, `test_mem_trim.py`, `test_ub_memo.py` and `test_ub_threads.py`, which read this code, pass. - In the game, a Steam Deck OLED, without a screen: 7 runs of 6a62d31c on the three scenes and 3 more with pictures, and more than 80 runs of the same commits on top of other renderer-only changes: no error, no GPU timeout in the kernel log. - The picture, by the renderer's own captures (`RX_VK_SHOTS`), standing still on the three scenes: main twice, then this PR, the last capture both runs have compared pixel by pixel (the mean absolute difference of a channel, 0 to 255). Two runs of main differ by 18.3 to 18.9 (a scene starts in one of two states and never stands still); this PR against the two runs of main: 21.9 and 17.3 in Hyrule Field, 17.8 and 7.9 in Kokiri Forest, 15.2 and 21.8 on the heavy view: as main against itself. - With the picture, by a person: a build holding this change with other renderer-only ones (3a3a6475, on main 514591a3) played on the Steam Deck OLED on 2026-11-14: nothing wrong seen on screen. - Linux: the back end builds (clang, on the Deck). Windows: `longshot_vk.dll` cross-builds with llvm-mingw 20261104 from a branch holding these commits and two more renderer-only ones (#238's and #384's), 0 failed. **Not tested:** - A PC whose pools have VRAM mirrors: `memory_rb_refresh()` takes its usual path there, which this does not change, but it was not run; the line changed after the measurement (above) is covered by `tests/test_parallel_rb_refresh.py` only. - Any hardware but a Steam Deck OLED; Windows in the game. **Needs other testers:** - Windows, any card: a short play, expecting nothing drawn differently and no slower frame (request to open) ## Screenshots Nothing on screen. ## Risk and rollback Each commit removes work and keeps the result, and each can be reverted alone. The two that are not mechanical: `memory_rb_refresh()` returning early (it trusts two counters: `st_mirrors` moves where a pool's `mstate` becomes `MIRROR_ON`, `st_mirrors_freed` in `mirror_free()`; a mirror that went another way would only lose the shortcut, since the counts would then never be equal); and `retired_imports_give_back()` passing the retired pools whose import is gone (it relies on a retired pool never being imported again: `Pool::memory` is only set where a pool is made). ## Reviewer notes - https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/231 (@dukenheimer) also edits `uniform_range()` (its c1 branch) and `payload<T>()`; I left `payload<T>()` out of this PR for that reason. The hunks do not overlap; whichever goes second rebases a few lines. - The back end is built one file at a time (no LTO), which is why `dump_on()` and the like are calls. An opt-in ThinLTO build measured about 1.5 to 2% fewer cycles on the same scenes on top of these; I can open that separately if it is wanted. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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 file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`; no appended row or assembled view). - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] Docs updated.
uniform_range runs for every uniform buffer of every descriptor build, about five a draw. Each one built and destroyed
a std::function for ub_overlay's allocator, which is only called when an overlay copy is made, and tested the bound
offset with % by an alignment the compiler does not know: a 64-bit division.

ub_overlay takes the allocator as a plain function and its alignment, which is stream_alloc as it is. The offset is
tested with a mask: the alignment is a power of two, Vulkan's minUniformBufferOffsetAlignment or 16, and
stream_reserve already refuses any other.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
A submission is replayed in parts (one every 120 work ops), and each part ran both under pools_mu:

- retired_imports_give_back summed every live pool's size (a std::map walk) for a log line written every 1,200 parts,
  and looked again at every retired pool whose import was long gone. The sum is now made for the line only, and the
  oldest retired pools without an import are passed once: the list only grows, and a retired pool is never imported
  again.
- memory_rb_refresh looked up every readback page among the pools to copy it into its pool's mirror. Where no pool
  has a mirror (integrated GPUs, GCN, RX_VK_MIRROR=0) every page ended at "no mirror", a map search each. With no
  mirror made that is not freed (a sparse mirror may hold no byte yet) it now returns, and still drops the pages not
  read for a while every 64th time.

tests/parallel_rb_refresh_test.cpp gets the mirrors' two counts and a case with no mirror: no copy recorded, and the
page still leaves the list.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
The renderer is built without link-time optimisation, so `bool dump_on() { return armed; }` in vk_dump.cpp was a real
call from the 32 lines that ask it in vk_draw.cpp and vk_replay.cpp: twice for every texture of every descriptor
build, in dynamic_state's fast path, and several times more a draw. The flag is now declared in vk_dump.h and
dump_on() is inline there.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
dynamic_state's fast path asks dyn_same at every draw whether the state is the draw before's: 13 floats, 35 words and
the raw bytes (depth, stencil, blend, the write masks), each in a loop that leaves at the first difference, one
compare and one branch at a time. For a draw that keeps its state, the usual case, that was every one of them. The
words and the bytes are now two memcmp; the floats stay compared by value (-0 is 0, a NaN is never the same).

tests/test_same_dyn.py also checks a difference in the last word's last byte, and -0 against 0.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
- draw() read use_counters.pipes_new, vk_programs.cpp's thread-local, before every pipeline lookup, for the program
  life log's "new pipeline" flag. ProgramUse is meant to cost one compare a draw with the log off: the counter is
  now read only when it is on.
- dispatch() formatted its program's key with snprintf at every dispatch to compare it with RX_VK_LOG_PUSH, set or
  not. Only with the switch now, as the storage buffers' peek already does.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
pools_mu, taken for every buffer a draw binds, is an SRWLOCK because that is half a std::mutex's cost on Windows. Off
Windows SRWLOCK is host/posix/windows.h's pthread_rwlock_t, whose write lock and unlock take more atomic steps than
a mutex's, and the lock is only ever taken exclusive. There it is a std::mutex now; Windows is unchanged.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
docs(tested): six small costs of the replay thread on a Steam Deck OLED
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
a41c8de8de
Main fbd42024 against the six commits at 6a62d31c, 7 pairs alternated on three scenes: the render thread's
instructions and cycles a frame, the replay time, fps, and the renderer's own pictures against two runs of main.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Contributor

Played with the picture on the Steam Deck OLED: a build holding these commits with other renderer-only ones (3a3a6475, on main 514591a3), nothing wrong seen on screen. Testing has it now; what is left before ready is a short run on Windows.

Played with the picture on the Steam Deck OLED: a build holding these commits with other renderer-only ones (3a3a6475, on main 514591a3), nothing wrong seen on screen. Testing has it now; what is left before ready is a short run on Windows.
First-time contributor

Windows compile and game-free checks for head a41c8de8:

  • longshot_vk.dll built with the pinned llvm-mingw 20261104 (RX_VK_ONLY=1 RX_RELEASE=1 RX_TARGET=windows, --jobs 6): 0 failed, linked.
  • tests/test_parallel_rb_refresh.py and tests/test_same_dyn.py: pass.

No gameplay run, so the short Windows run you mention is still open; this only covers the build and the two checks.

Windows compile and game-free checks for head `a41c8de8`: - `longshot_vk.dll` built with the pinned llvm-mingw 20261104 (`RX_VK_ONLY=1 RX_RELEASE=1 RX_TARGET=windows`, `--jobs 6`): 0 failed, linked. - `tests/test_parallel_rb_refresh.py` and `tests/test_same_dyn.py`: pass. No gameplay run, so the short Windows run you mention is still open; this only covers the build and the two checks.
p0lux changed title from WIP: perf(vk): six small costs of a draw and of a submission's parts removed to perf(vk): six small costs of a draw and of a submission's parts removed 2026-11-14 14:32:14 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'perf/six-cpu-costs:p0lux-perf/six-cpu-costs'
git switch 'p0lux-perf/six-cpu-costs'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!391
No description provided.