perf(vk): skip the pipeline cache file on Linux NVIDIA PCs with 20 GB of RAM or less (0.7-0.8 GB less) #416
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!416
Loading…
Reference in a new issue
No description provided.
Delete branch "tonesi/Longshot:perf/388-pcache-low-ram"
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 16 GB Linux PC the game's memory outgrows RAM and swap fills as areas change
(#388). About a gigabyte of it is the pipeline cache's file, which is read
whole at every start and kept by the driver in the game's private memory, although NVIDIA's own shader cache already holds the
same pipelines. Not reading it there takes about 0.7-0.8 GB off the game's footprint on that PC.
What changed
pipeline cache:
pcache_load_off()ingpu/vk/vk_draw.cppdecides throughgpu/vk/pcache_load.h(new, no Vulkan in it).Where
RX_VK_PCACHE_LOADis unset, the file is not read only when all of these hold:programs_load_at_create());driver_cache_bytes(), walked only when the rest already holds);--prepare-shadersrun.Everywhere else the file is read as before. One log line gives the decision and its reason.
pipeline cache: with the file not read (this rule or
RX_VK_PCACHE_LOAD=0),play_files_merge()no longer merges the playfiles into the cache. Before, that cache started empty, and at 16 play files the merge wrote it over the file: a session
measured here wrote 38 MB over a 1.2 GB file, which a start loading programs before the title then lacked. The play files
stay on disk for a start that reads the file.
play_cache_save()still frees play's generations at each save but writesnone, because the driver's own cache keeps those pipelines. This is what the switch's description already said: "the cache
starts empty and is not saved".
tests:
tests/test_pcache_load.py(new) compiles and runs the rule's table and checks thatvk_draw.cppdecides through it.tests/pipeline_play_cache_test.cppandtests/test_pipeline_play_cache.pygain ano-savecase. It fails onmain(theplay files are merged, and folded into the file at 16) and passes here.
docs: an
RX_VK_PCACHE_LOADrow in docs/development.md's GPU switches.Flags and settings
LONGSHOT_PREPARE_ONLY--prepare-shaderssets for its own process. Now also read bypcache_load_off(), so a preparation reads and saves the file whatever the rule says--prepare-shadersRX_VK_PCACHE_LOAD=0or10), or read as before (1)1to read it on the small PC, for an A/BMeasurements
main:
0caf0618(2026-11-14)this PR: perf/388-pcache-low-ram @ 999bc37f (on
0caf0618; the branch is now73035089, the same change rebased onto main5e072fc8, whose two new commits change the temporal pass and a shader pass, nothing the pipeline cache, program loads or memory go through, so these numbers still hold)Hardware: NVIDIA GeForce RTX 3070 Ti 8 GB (also drives the desktop), driver 595.104.02; Intel Core i7-12700K; 16 GB RAM (15,757 MB usable), swap 15.4 GB zram + 4 GB partition; Pop!_OS 24.04 (COSMIC), kernel 7.2.15
Scene: a teleport tour from a child save at Zora's River: Zora's Domain, Hyrule Field, Kakariko, the Market, Kokiri Forest, Lake Hylia, Death Mountain Trail, Goron City, 40 s each with a scripted walk and camera turns (about 7 minutes). Windowless (headless renderer), Quality preset Auto (this PC), which picks tier 3 here and so the reporter's
memory_budget=3; output 2560x1440; no DLSS (a source build without it); programs loaded at create (the default with mesh shaders)Method: a full build of main and an incremental build of this branch from the same work folder, each in its own game folder; main prepared, then its caches, NVIDIA cache and options handed to the PR's folder (same shader stamp: this PR leaves
vk_api.halone), as an update keeps them. Three interleaved runs per side (main, PR, main, PR, main, PR), each after RAM had settled to 9 GB+ available. The game's memory every 2 s from/proc/<pid>/statusand/proc/<pid>/smaps_rollup(resident + swapped, file pages left out), the system's from/proc/meminfo; loads and waits from the log's close census; frames from the renderer's frame log (fps from 60 s on, an observation on the desktop's card, not a timing run)How to reproduce: on a 16 GB Linux PC with NVIDIA, a prepared folder, the same tour (the debug teleport through
scripts/longshot_ctl.py teleport), with and withoutRX_VK_PCACHE_LOAD=1; compare the game'sRssAnon + RssShmem + smaps SwapandMemAvailable0caf0618(3 runs, median)With NVIDIA's cache emptied (a fresh
__GL_SHADER_DISK_CACHE_PATH, one run each), the PR reads the file as main does. OnNVIDIA the file then answers almost nothing: main had 10,143 graphics pipelines in no cache, the PR 10,901. A cold driver
cache is as bad on both sides, and this PR doesn't change that path.
Not this PR's numbers, measured on the same PC and tour for the issue: with programs loaded before the title (the
reporter's install), the game's total peaks at 16.2 GB and MemAvailable reaches 0, because the shader modules of about 19,400
programs take 8.5 GB of private memory. Not reading the file there saves about 0.2 GB and doubles the precompile (29 s against
15 s), so the rule leaves start mode alone. #223 is the change for that.
Testing
Tested:
run each with NVIDIA's cache emptied. Every run reached all eight areas, with no device lost and 0 kernel or NVRM lines.
longshot_vk_pipelines.binthrough all its sessions, with no play file written or folded.Main's folded its play files into it at 16, as before.
RX_VK_PCACHE_LOAD=0with programs loaded before the title wrote a 38 MB cache over the1.2 GB file (the play-file fold this PR stops).
tests/test_pcache_load.py,tests/test_pipeline_play_cache.py(13 cases,no-savefailing on main'svk_draw.cpp) andtests/test_pcache_stamp.py, with g++ and clang.4fd82b75(this branch rebased on5e072fc8; Linux,python -I -B tests/run_all.py --jobs 8): 529 passed,3 failed, 30 skipped, as before the rebase. All three fail the same way on main
0caf0618:tests/test_framegen_resources.pyandtests/test_module_ids.py(the fixtures#350 restores), and
tests/test_pad_layout.py.longshot_vk.dll) of4fd82b75compiled and linked on this PC as CI's build-windows.yml builds it:RX_TARGET=windows RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalonewith the pinned llvm-mingw 20261104 (Linux build,scripts/builder_package.py llvm-mingw --host linux): 0 failed. The change adds no path handling.Not tested:
driver_cache_roomraises to 10 GiBlike NVIDIA's, so the same default may be right there, but it is unmeasured.
Needs other testers:
pipeline cache file not readline, nolisted pipelines ... in no cacheat the close, and no new stutter againstRX_VK_PCACHE_LOAD=1(T-XTVW)Screenshots
Nothing on screen.
Risk and rollback
in-memory copy. Each load is slower (median 5.0 against 1.8 ms, on the load workers), and draws waited for a load a little
more (10 against 2 per tour, 206 against 52 ms in all). Frames over 100 ms were 29 against 26; fps was the same.
RX_VK_PCACHE_LOAD=1reads the file again.NVIDIA the file answered almost nothing with the driver's cache cold, so this costs nothing beyond what main costs there.
Reviewer notes
gpu/vk/pcache_load.h, thenpcache_load_off()and the two early returns inplay_files_merge()andplay_cache_save().waited 6 and 5 times (251 and 247 ms); with 4 throughout (2 runs), 4 and 8 times (209 and 218 ms), each load slower (7.0 ms
median). Neither beat the 197-276 ms of the default 2.
MALLOC_ARENA_MAX=2and more frequentmalloc_trim, and giving back freed guest pages around imports. That last one was ahost change; the game re-mapped what it freed at once.
#223 works on; 1.4 GB over the tour with programs loaded at create); and
the load-start marker's switch to start mode costing 16 GB PCs about 5 GB for good.
Issues
See #388
See #223
See https://github.com/NotRexed/Hookshot/pull/2206
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).mainand passing here.tests/that reads changed source updated in this PR, not weakened.🤖 Generated with Claude Code
Description completed: the measured commit named on the 'this PR' line (999bc37f, rebased since onto
5e072fc8unchanged, and why the numbers still hold), LONGSHOT_PREPARE_ONLY in the flags table, the Windows back end compiled and linked with the pinned llvm-mingw (Linux build, as CI does), and test request T-XTVW for a longer session on another 16 GB Linux NVIDIA PC.WIP: perf(vk): skip the pipeline cache file on Linux NVIDIA PCs with 20 GB of RAM or less (0.7-0.8 GB less)to perf(vk): skip the pipeline cache file on Linux NVIDIA PCs with 20 GB of RAM or less (0.7-0.8 GB less)Read head
4fd82b75against main5e072fc8: the complete eight-file diff, description, sole comment, empty reviews, issue 388 and both comments, cache readers/writers, program-load dispatch and the relevant changes in 415 and 370; ran nine game-free check invocations across five files, all passed, with GCC and Clang for the compiled checks. No game, full build or timing run.What's good
vkCreatePipelineCache; file-backed input avoids a large temporary byte vector, not the driver's retained cache. The reported memory reduction is consistent with removing that retained input.play_files_merge()prevents an empty base cache plus 16 rolling files from replacing the larger base file. The newno-savecase passes with both compilers and preserves the authored base and rolling files.Must change
Reach generation retirement from the ordinary save entry point. In
gpu/vk/vk_draw.cpp, lines 945-949, the new no-save branch destroys retired play caches, but the periodic caller cannot reach it:vk_replay.cpp, line 4099, callspipeline_cache_save(false), whose first condition atvk_draw.cpp, line 10343, returns whenpcache_no_saveis true.play_cache_begin()still creates a generation, andcreate_cached()keeps adding play pipelines to it. On the PCs newly selected by this default, those additions therefore stay in the same generation until close, instead of being retired at the periodic saves as on main. Route generation retirement around the file-write prohibition while keeping the current user-count synchronization. Extend the fixture throughpipeline_cache_save(false), after enough new pipelines to trigger a save, rather than callingplay_cache_save()directly. It should prove that the old cache is destroyed, new work still succeeds, and no file is written. Traced through the source; the passingno-savecase does not exercise this caller. No long-session memory run.Use the selected driver's cache for the cold-cache guard. In
gpu/vk/vk_draw.cpp, lines 1055-1057, the new decision treatsdriver_cache_bytes(nullptr)as NVIDIA's cache size. That helper, lines 9619-9633, adds NVIDIA's and Mesa's folders together. A fresh, empty__GL_SHADER_DISK_CACHE_PATHwith at least 512 MiB left in Mesa's folder therefore selects “file not read”, even though NVIDIA's cache is empty. This is not just the disclosed case of other programs filling NVIDIA's cache: an unrelated driver's bytes defeat the explicit cold-cache fallback. The base and rolling files are then ignored, and listed pipelines which neither live cache can answer are deferred to their first draw or dispatch (vk_programs.cpp, lines 240-255). Count only the selected driver's cache for this decision and treat an unavailable scan as unknown/cold. Add an adapter-level case with an empty NVIDIA folder and a populated Mesa folder, requiring the file to be read. Traced through the source, not reproduced in a game; the new table test supplies a number directly and never exercises folder selection.What the condition actually selects
#ifndef _WIN32, and the GPU check isvendorID == 0x10DE. On the supported platforms this means Linux with an NVIDIA GPU, not specifically NVIDIA's proprietary driver. Mesa NVK is not excluded by that check;dev.driver_idis already available if proprietary-driver-only is the intended scope.sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGE_SIZE), shifted right by 20. It is total RAM reported by the OS, not current free RAM, VRAM or a process memory allowance. Unknown RAM falls back to reading the file. The inclusive limit is 20,480 MiB, or 20 GiB, with the byte count rounded down to MiB. A typical 16 GiB PC qualifies, including the reported 15,757 MiB machine. Typical 24 and 32 GiB PCs do not; unusually large hardware reservations can change the reported total.programs_load_decide()runs after device creation and before this choice. The existing load mode matters: native mesh normally selects create, while the fallback, compressed program cache or an existing load-start marker can select start. This PR does not help the issue's original load-before-title configuration by default.RX_VK_PCACHE_LOAD=0,off,falseornoforces the skip;1or another non-off value forces loading. Empty is treated as automatic. The switch wins over preparation too:choose(0, true, ...)skips the file, and the new test explicitly requires that. The documentation's “always by --prepare-shaders” needs that exception.Cold, disabled or unwritable driver caches
A genuinely empty aggregate scan falls back to main's file-loading path. A first start without a valid warm marker also has the existing up-front preparation/warm pass, so it is not correct to say every cold first start necessarily compiles everything in play.
The size check does not establish that this game's entries are present, compatible or usable. It does not check
__GL_SHADER_DISK_CACHE=0, and it does not establish usability on a read-only home. Read-only storage alone does not prove existing entries cannot be read. NVIDIA also documents that its cached binaries are driver-version-specific: https://download.nvidia.com/XFree86/Linux-x86_64/570.124.04/README/openglenvvariables.html. These cases need separate evidence, not a directory-size inference.With an existing warm marker, a skipped file and driver misses, loads at create only probe the caches. Missing pipelines are compiled when first needed in play. More than 500 listed misses can write
longshot_load_start.txt, making the next run load before the title and disabling this automatic memory saving. The description discloses unrelated NVIDIA entries, but “this costs nothing beyond main” is not established for disabled caches, read-only storage or other drivers.Files retained for the next run
When the file is skipped, neither the base file nor new rolling files are written. Existing published files and their stamp are left alone; old temporary-file cleanup still runs. A later run that reads the file can reuse the retained base and rolling files if their translator stamp and Vulkan vendor/device/cache UUID still match. Otherwise the normal stamp/header checks reject them. This preserves an existing valid cache, but does not keep it updated with the skipped run's new pipelines or make an old-driver file valid for a new driver.
The explicit
RX_VK_PCACHE_LOAD=0overwrite protection does work independently of the new default. The new retirement branch also stops the close path from writing a rolling file. The periodic-retirement problem in finding 1 is about retained memory, not a remaining overwrite path.Measurements
The reported warm-cache comparison meets the important paired-memory conditions: named builds, the same 16 GB PC and settings, and three alternating tours per side with medians. Arithmetic from the committed run rows gives:
The first two ranges are disjoint and their median gaps exceed their within-side spreads. That supports the claimed memory reduction on this PC and tour. MemAvailable has overlapping ranges and substantial variation; it does not establish that memory pressure is solved. The reported fps difference is smaller than the spread and was explicitly not collected as a controlled timing comparison. There is no demonstrated speed gain. The reported program-load median worsens from 1.8 to 5.0 ms, and total first-use waits from 52 to 206 ms; retain all three runs' load and wait rows to show their spread, not just one excerpt and the medians. No overall startup-time improvement is shown.
A cold-driver-cache run was measured, once per side, with 10,143 and 10,901 graphics misses. However, the candidate read the file in that run. It tests the cold fallback, not the behavior of skipping the file while the relevant driver cache is cold or disabled. It also lacks repeated cold pairs.
Correct the rebase explanation. Main's intervening change includes
gpu/vk/eden/shader_recompiler/ir_opt/scene_tonemap_pass.h.nx2/build.py, lines 1178-1182, hashes every file underedeninto the shader stamp. That change therefore does go through cache invalidation, even though the pipeline-loading code is unchanged. Keep the numbers labelled as measured on999bc37f; explain the changed cache identity and distinguish warmed steady-state results from the first start after updating. The measured commit was not available from the fetched refs or the public commit endpoints, so its claimed identical replay was not independently established here. Full per-run logs and memory samples are not linked in the thread.Overlaps and remaining checks
pcache_init(). This PR deliberately keeps generations. Do not combine those approaches unchanged: 415's guard would prevent the retirement design here from running at all and send new pipelines into the base in-memory cache. Their switch documentation and fixture cases also need one combined definition. No combined build or test was run.vk_api.hdoes not exclude the changed Eden header above. Installing its new formula causes a one-time invalidation. While this PR skips loading, the old pipeline files and stamp remain untouched; on a later loading run, normal stamp rejection still applies. Do not relabel old warm-cache evidence as a combined-stack update test.test_pcache_load,test_pipeline_play_cache(13 cases),test_pcache_stampandtest_pcache_fetch_lock, each with GCC and Clang, plustest_gpl_library_cache. No test was weakened. These are rule, file-model and source checks, not a native NVIDIA-cache reproduction.The same idea measured on AMD with RADV, a Steam Deck OLED, in case the rule goes past NVIDIA. Not this PR's code: the switch main already has,
RX_VK_PCACHE_LOAD=0, against unset.Setup. Steam Deck OLED (AMD Custom APU 0932, RADV VANGOGH, Mesa 26.2.0-devel), 16 GB shared with the GPU, SteamOS 3.9.5, kernel 7.2.7-valve2. A renderer built from main
514591a3with renderer-only changes of other pull requests (#238, #384, #387, #391, #223's commit), programs loaded at create (RX_VK_LOAD=create), Minimum preset, handheld profile, no screen (headless). The pipeline cache file is 307 MB there. Each run starts the game and loads a save by script; memory is read from/proc/<pid>/statusand/proc/meminfoat the end of the measured span, KiB divided by 1,024.What it gives back (
RssAnon, MiB, file read then not read):~/.cache(1,489 MB)About 200 to 300 MiB for a 307 MB file.
MemAvailablefollows (heavy view 2,488 / 2,652 against 2,762 / 2,802 MiB). GPU memory (drm-memory-gtt) does not move, and standing still the frame rate is the same (29.28 / 29.57 against 29.46 / 29.36 fps).What it costs here.
RX_VK_FRAME_TRACE=1, time spent in frames over 60 ms): Kokiri Forest 5.5 s read against 3.1 and 6.0 s; heavy view 3.2 against 3.4 s; Hyrule Field 2.5 / 2.8 / 3.5 / 2.6 s read against 3.0 / 3.4 / 3.8 / 3.5 / 2.6 s. In Hyrule Field the longest frame is about 1 s in 4 of the 5 runs without the file (514 to 1,011 ms) against 1 of the 4 with it (529 to 1,118 ms). Small samples, but on this 4-core machine the workers building pipelines seem to cost the replay while it uploads the area's textures.For the driver cache guard (the review's point 2). On a Deck in Game Mode, Steam points Mesa at a cache of the game's own,
steamapps/shadercache/<the shortcut's id>/mesa_shader_cache_sf(a single-file cache, throughMESA_SHADER_CACHE_DIR), not at~/.cache/mesa_shader_cache. On this Deck the first holds 187 MB and the second 1,489 MB (filled by headless runs started over SSH). The 187 MB one answered every pipeline in the runs above: the copy grew by 3 MB in its first run and not after. So a 512 MB threshold on a folder's size would look at the wrong folder here, and would turn down a cache that is complete. If the rule goes to Mesa, the folder to read is the oneMESA_SHADER_CACHE_DIRnames when it is set.Not tested: this PR's code; a start from Steam itself (its cache was a copy, read through
MESA_SHADER_CACHE_DIRandMESA_DISK_CACHE_SINGLE_FILE=1in a headless run); a cold Mesa cache; a long session. The game folder's cache files were copied aside and put back after each series.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.