feat(vk): add opt-in VRAM compaction and hold the driver's count below its limit #1821
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!1821
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/vram-compaction"
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?
At DLAA on a 12 GB card the game alone fills VRAM within a few minutes of walking, and from then on the GPU falls behind and frames wait for it. With
RX_VK_COMPACT=1the textures' unused space goes back to the driver and the texture budget keeps the total clear of the limit. Off by default.Tracking: #995 (FPS drops in Hyrule Field); related to #993 (VRAM over budget).
What happens now
The driver counts VMA's blocks whole. An evicted texture's memory stays inside its 128 MB block, so the count keeps what the cache gave up. In a five-minute walk across Hyrule Field at DLAA 2560x1440 (RTX 4070 SUPER, 10.2-10.5 GB allowed) the count sits at 9.5-10 GB, 1.4-1.7 GB of it free inside the blocks, and:
The change (all behind
RX_VK_COMPACT=1)RX_VK_COMPACT_MB; 192 MB while the count is over 90%), VMA's defragmentation moves textures out of sparsely used blocks, at most 12 textures and 64 MB a present: a new image at the new place, the texels copied on the GPU in the present's upload commands, the views made again when next asked for, the old image destroyed once the GPU is past the copy. VMA then frees the blocks that emptied. While a pass is open nothing of the pool is freed.RX_VK_COMPACT_PCTpercent of the allowance (85), letting go of the excess alone rather than cutting the cache to 7/8.gpu/vk/vram_compact.h; the switches are indocs/development.md; the runs are indocs/tested-configurations.md.Numbers
Built from this branch alone at
caaee0b. Unattended runs with scripted input, a five-minute walk across Hyrule Field from a save in the field, DLAA 2560x1440, RTX 4070 SUPER (driver 617.42), Ryzen 9 9950X3D, Windows 11, other programs open (10.2-10.5 GB allowed). Frames after the load, fromRX_VK_FRAME_TRACE. The walk's path drifts between runs, so two runs a row. A window is 120 presents.RX_VK_COMPACT=1RX_VK_ASTC_BC_ALL=1(one run)No validation, device-lost or "not made" line in any run.
The branch was moved onto today's main (
484cfd1) before its first push. #1506's squeeze rule, merged since, stays as it is beside the hold. The checks were run again there; the game was not measured again there.Limits
RX_VK_PARALLEL.Test
python -I -B tests/test_vram_compact.py(the rules' arithmetic, and source checks that the compaction stays opt-in, that the dead list waits while a pass is open, and thatcreate()'s own lines are as they were). Of the 26 game-free checks that read the changed files, 19 pass on my PC; the other 7 fail the same way on plain main there (no g++, no POSIX signals), so CI is the judge of those.🤖 Generated with Claude Code
Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
I tried this branch (
74d0936, built as-is) on Linux with an RTX 3090 (24 GB), DLAA at 2560x1440, in Kokiri Forest.RX_VK_TEST_ALLOWED_MB=6000simulates a smaller card. Results so far:Correctness: works on Linux. With
RX_VK_COMPACT=1and a 6 GB allowance, compaction ran (e.g. "159 textures moved (160 MB) in 15 passes, 0 left where they were; 2 blocks (256 MB) given back"). There were no validation, device-lost or "not made" lines, and screenshots taken after the passes show no wrong or garbled textures.A/B, same build, only
RX_VK_COMPACTchanged. Last 3000 frames of each run (frame CSV):Caveats: two runs per case, one scene (not Hyrule Field), and a simulated allowance on a big card, not a real small one. A 4 GB-allowance pair is running now. I'll follow up with those numbers and, if I can pin down the cause, a suggested fix. As it stands, this looks fine as opt-in but I wouldn't make it the default yet.
A follow-up and a correction to my earlier comment. With more runs, the stutter cost I reported doesn't hold up: it was run-to-run noise.
Same setup as before: Linux, RTX 3090, DLAA 2560x1440, Kokiri Forest,
RX_VK_TEST_ALLOWED_MBto simulate a smaller card, last 3000 frames of each run.6 GB allowance, 4 runs each way (two batches):
RX_VK_COMPACT=1The same setting swings ~33-39 fps and ~60-280 slow frames between runs, more than any off/on difference. The multi-second freezes happen with compaction off too, so they aren't from this PR. A variant with the copy passes disabled was no better. I can't see an fps or stutter cost from compaction here. The VRAM saving is real: ~100-300 MB less used at the end of each run (e.g. 5630 → 5342 MB of 6000).
4 GB allowance (soft, 2 runs off / 2 on): off 31.4 / 32.3 fps, on 30.6 / 29.7 fps, ~300 MB less VRAM with it on. Same picture: within the noise. Note the soft allowance doesn't stop the game going over it (4.4-4.75 GB used), so this isn't real pressure. A run with ~4 GB of real free VRAM (another process holding the rest) is queued, and I'll add it when it's done.
Correctness: no validation, device-lost or "not made" lines in any run, and no wrong textures in the screenshots after passes.
On whether to default it on: I'd keep it opt-in for now. It costs nothing on big cards because it never triggers, but it moves live textures, and it has only been tried on NVIDIA (your 4070 SUPER on Windows and this 3090 on Linux). The cards that would benefit most are often AMD cards and integrated GPUs. A few runs there and on a real 4-8 GB card would be the thing to see before turning it on automatically, e.g. only below ~12 GB of VRAM.
The last runs, as promised: a real 4 GB limit. Another process held 17.6 GB of the 3090's VRAM, leaving ~4.1 GB free, so the driver's own allowance was 4.0-4.2 GB (no
RX_VK_TEST_ALLOWED_MB). DLAA 2560x1440, Kokiri Forest, two pairs:RX_VK_COMPACT=1In both modes the game ran over the limit. Large images and mirror pools ended up in system memory, GPU time was 28-53 ms, and there were 1-1.3 s hitches, but no device lost and no crash. Compaction never started in these runs, so on vs off here is just noise. At this resolution a hard 4 GB limit is past what compaction can help with. A real 4 GB card would also differ: less bandwidth, and Windows pages VRAM out rather than falling back the way Linux NVIDIA does.
One more note on my earlier runs. Part of the first on/off gap at 6 GB came from the two modes ending up in different scenes: Saria's dialogue (~1800 draws a frame) vs after it (~2400). With every run reaching the same scene, compaction on was no worse than off, and GPU time was the same (~15-16 ms). The occasional 0.3-2.5 s freezes were CPU contention on my PC (other jobs running), with compaction on and off.
Summary for my setup: correct on Linux/NVIDIA. It does nothing at 24 GB because it never triggers. At smaller allowances it saves 100-250 MB with no fps change in Kokiri Forest. Your Hyrule Field gains are the case I couldn't reproduce here, so runs from someone on a real 8-12 GB card in the field, or on AMD/Intel, would be the useful next data.
Code review. My two earlier comments cover the test runs (Linux, RTX 3090). I'd keep this opt-in until it has run on AMD, an iGPU and a real 4-8 GB card.
I read the diff against
b2f1f5fand checked these:RX_VK_COMPACTunset,compact_stepreturns at once. The dead-listbreaknever fires.reclaimstaysblock_free, sopressure, the 90% rule and the 97% squeeze are the same as before.evict_tostays 0, so the eviction loop still stops atbudget / 8 * 7.create_imagegetstextures=false, so the pool choice and the allocation flags don't change.texture_poolis null, so the block log skips it. The only always-on lines arex.create_flags,present_sourceandcompact.pressed, and they're just bookkeeping.create()used: flags (cube, 2D_ARRAY_COMPATIBLE, MUTABLE / BLOCK_TEXEL_VIEW, EXTENDED_USAGE), type, format, extent after skip, levels, layers, samples, usage, OPTIMAL tiling, exclusive sharing.create()adds no pNext, and the BC/ASTC usage fallbacks never land in this pool, sox.usageis the image's real usage. It's bound withvmaBindImageMemory(dstTmpAllocation). Every image here stays in GENERAL, so UNDEFINED->GENERAL on the new image and GENERAL->GENERAL copies of every level are right. The full barriers before and after are both kept (vk_work.h counts the copies, so the second barrier isn't deduped). Everything runs on one queue, so there's no ownership transfer. The copy goes into the present'sr.upload, which is submitted beforer.cmd. The only imager.cmdtouches is the presented one, andpresent_sourceexcludes it, including thedirect_viewand fg_after views. VMA marks the block of an ignored move immovable, so a skipped texture can't make passes loop.completed >= compact.value, beforevmaEndDefragmentationPass, which matches VMA's rule.destroy_gen++makes the draw's handle cache, the descriptor slots and the temporal/FSR depth views re-resolve. A moved texture getslast_value = rec_value, so eviction and write-back skip it until the pass has ended (compact_stepruns before the eviction loop in the sametexture_frame). Holding the dead list while a pass is open coversdestroy_laterof a moved texture. The other frees in this file (target pool, BC scratch) are in other pools. A move whose allocation isn't intextures(one on the dead list) is ignored, which fails safe.do_presentunderreplay_mu. A parallel replay's segment workers have finished by then, so nothing else touchestexturesor the defrag context.used - min(used, reclaim),total > over ? ... : 0). WithoutVK_EXT_memory_budget,knownis false, so the hold is off and only the free-space trigger runs. On an iGPU the existing 2.5 GB texture cap applies first.tests/run_all.py, and of the 20 commits main has gained since, onlydocs/development.mdoverlaps (GitHub says mergeable). The CI failures (test_antilag, test_nup_out_of_space, test_wayland_libdecor_hint, test_draw_parameters) are in tests main has changed since;test_vram_compact.pypasses on both OSes.Nits, none blocking:
room_in_blocksstill counts free space in every pool. With compaction on, the PR's own argument says only the textures' pool counts. The 85% hold mostly acts first, so it's moot in practice.compact_step; a small helper would remove the copy.@ -0,0 +9,4 @@import tempfilefrom pathlib import PathRUN_ALL_ARGS = ["--cxx", "g++"]Nit:
"{cxx}"here (and no hard default below), as the #1822 convention does: run_all fills it fromCXX/ clang++ / g++ / c++. With"g++"hard-coded, a machine that only has clang++ (llvm-mingw on Windows, or your PC as you noted) skips the arithmetic check instead of running it.Tested on a real 6 GB card.
Setup: RTX 4050 Laptop (5152 MB allowed), Core 5 210H, 16 GB RAM (single-channel), Windows 11, Hookshot v2.1.0, 1080p, DLSS, Low (6GB) preset, frame generation off,
RX_VK_WB_MAX_KB=1024. Same area each run.Results:
Compaction off,
RX_VK_MIRROR_MB=1300: 35-42 fps. VRAM 5.2-5.7 of 5.0 GB, textures 500+ MB over budget ("not squeezed"), 8 pools in system memory. Most slow frames were memory sync.RX_VK_COMPACT=1only: 19-23 fps. VRAM stayed under the limit, but the mirrors dropped to 512 MB, so GPU time went from 15-18 ms to 36-45 ms.RX_VK_COMPACT=1+RX_VK_COMPACT_PCT=90+RX_VK_MIRROR_MB=1300: 33-46 fps. Best result. Memory-sync slow frames went from 45 to 7, and camera turning is much smoother. VRAM still slightly over (5.1-5.5 GB).Same as 3 +
RX_VK_ASYNC=1: 26-29 fps. Steadier frame times, but a translucent white flicker appears over ground textures.Summary: On a 6 GB card, compaction frees space by shrinking the mirrors instead of the textures, which makes it slower. Keeping the mirror budget at 1300 MB fixes that. Logs for 3 and 4 are attached.
hookshot_log_async_flicker.txt
hookshot_log_compact_mirror1300.txt