perf(vk): six small costs of a draw and of a submission's parts removed #391
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!391
Loading…
Reference in a new issue
No description provided.
Delete branch "p0lux/Longshot:perf/six-cpu-costs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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_range()ingpu/vk/vk_draw.cpptested a bound offset with%by an alignment the compiler does not know (a 64-bit division), and built and destroyed astd::functionforub_overlay()'s allocator at every bind, about five a draw. Now a mask (the alignment is a power of two: Vulkan's, andstream_reserve()already refuses another), andub_overlay()(gpu/vk/vk_replay.cpp,vk_draw.h) takes a plain function and its alignment.gpu/vk/vk_memory.cpp, at everymemory_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.dump_on()wasreturn armed;ingpu/vk/vk_dump.cpp, a call from some 30 lines ofvk_draw.cppandvk_replay.cpp(twice a texture of every descriptor build); it is inline invk_dump.h.dyn_same()(vk_draw.cpp) compared 35 words and the raw bytes one at a time at every draw; twomemcmp, the floats still by value.draw()readuse_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 forRX_VK_LOG_PUSH. Only with the log on.pools_mu(vk_memory.cpp) is an SRWLOCK, which off Windows ishost/posix/windows.h'spthread_rwlock_t, dearer than a mutex for a lock only ever taken exclusive; there it is astd::mutex. Windows is unchanged.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/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(db2d4073toc0598cbc) 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.his 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 elsewhereHow 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 buildfbd42024The 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:fbd42024+ #238 @ 3a6fbc0cTesting
Tested:
python -I -B tests/run_all.pyon macOS with the Vulkan headers given: 485 passed, 37 failed, 39 skipped; 36 of the 37 fail the same way on main0caf0618on that machine (no/proc, noposix_fallocate, Python 3.9, the Linux launch scripts and the like:nx2/test_nup_zstd_heads.py, and intests/: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.pyandtest_ub_threads.py, which read this code, pass.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.514591a3) played on the Steam Deck OLED on 2026-11-14: nothing wrong seen on screen.longshot_vk.dllcross-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:
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 bytests/test_parallel_rb_refresh.pyonly.Needs other testers:
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_mirrorsmoves where a pool'smstatebecomesMIRROR_ON,st_mirrors_freedinmirror_free(); a mirror that went another way would only lose the shortcut, since the counts would then never be equal); andretired_imports_give_back()passing the retired pools whose import is gone (it relies on a retired pool never being imported again:Pool::memoryis only set where a pool is made).Reviewer notes
uniform_range()(its c1 branch) andpayload<T>(); I leftpayload<T>()out of this PR for that reason. The hunks do not overlap; whichever goes second rebases a few lines.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
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md; no appended row or assembled view).tests/that reads changed source updated in this PR, not weakened.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]>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.Windows compile and game-free checks for head
a41c8de8:longshot_vk.dllbuilt 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.pyandtests/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.
WIP: perf(vk): six small costs of a draw and of a submission's parts removedto perf(vk): six small costs of a draw and of a submission's parts removedView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.