perf(vk): write the pipeline cache only when its data changed (no 1.1 GB rewrite at every start) #1660
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1660
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/pipeline-cache-unchanged"
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?
The problem
Every start rewrote the whole pipeline cache,
mkw_vk_pipelines.bin(1.1 GB here), shortly after the title screen, even when nothing new was in it.pcache_built_since_savecounts every pipeline made with the app's cache, including the ones the cache already held. The precompile makes about 13,000 pipelines at each start, all of them found in the cache. That puts the count over 32, so the first check in play (present 300) fetches and writes the whole cache. The file came out at the same 1146 MB, +0 KB, at every start.On a SATA SSD (90% full, where this game is installed on my PC), that write took 3.6–9.9 s. It also left the disk's 4 KB reads at 10–450 ms for minutes afterwards. The game's own reads from that disk (code pages of
hookshot.exe, the romfs) waited behind them, and those waits are the long frames.The fix
pcache_initrecords the size of the data it read.fclosenow also counts as a failed write. Before, the file was renamed into place anyway.vk: pipeline cache saved (N KB).I tried counting only pipelines new to the cache, using
VkPipelineCreationFeedback'sAPPLICATION_PIPELINE_CACHE_HIT_BIT. It cannot tell them apart: NVIDIA's driver reports most precompiled pipelines as not found in the app's cache (about 460 of 12,900 here), because it then serves them from its own. The data's size is what the file would actually gain.The pipeline cache only grows as pipelines are taken in. If a driver ever kept the same size with different contents, the cost would be a skipped save: those pipelines would compile again at the next start. Nothing breaks.
This works with #1586: its forced save after warming the libraries still writes when the warm added data.
Measured
Test setup:
size asked in 0.0 ms)The average stays at 60 fps (capped). The "before" runs also had line-by-line log writes; a separate series with this fix in both arms shows those cost much less than the cache rewrite.
Tests
tests/test_pipeline_cache_save.pyhas a new case,unchanged:The existing cases now grow the data between saves, where they expect a second write.
--baseline-sourceset to4518a20^'svk_draw.cpp(the helper the option is meant for), all 12 pass, the negative ones included.main's function failsunchanged.Also checked:
RX_VK_ONLY=1 nx2/build.py --standalone: 205 files, 0 failed, no warnings invk_draw.cpp.tests/run_all.py: 224 checks, 33 skipped, 2 failed for reasons of this PC, not this change:test_precompile_compute_guard.pyneedswindows.hfor an MSVC target.test_nup_paths.pyneeds the symlink privilege.Left for later
A save in play that does have something new still fetches and writes the whole file. A player's first session after an update can make about 12 such writes. A session cache for the pipelines the base cache misses, merged into the base at exit, would remove those. That is a larger change, so it is not in this PR.
🤖 Generated with Claude Code
Looks good. The size-first check is cheap (no data copied), the saved size is only updated once write, fclose and rename all succeeded, and counting a failed fclose as a failed write is a real fix. The new
unchangedcase covers the failed-write, failed-rename and smaller-size paths, and the existing cases were adjusted sensibly. CI is green.Two small notes, nothing blocking: