perf(vk): keep the write-back shadows while VRAM is full of free space inside VMA's blocks #1637
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!1637
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/shadows-by-allocated"
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?
After some minutes of play on a 12 GB card with other programs open, the frame rate drops and stays down for the rest of the session. The renderer gives up a speed-up (the write-back shadows) when VRAM looks full, although much of that is free space it is holding itself.
What happens now
The write-back diff's shadows are given back when the driver's count of the process's VRAM passes 95% of what it allows (
shadows_check_room), and made again only under 85% (shadow_room).On a 12 GB card the texture cache fills VRAM to 93-98% of the allowance within minutes of play. Much of that is free space inside VMA's blocks, which evicting textures does not give back to the driver. So the shadows (272-448 MB) go and never come back, and every write-back is a whole copy again for the rest of the session.
In my own logs (RTX 4070 SUPER, 2560x1440, other programs open, 10.4-10.7 GB allowed) the line
the shadows given backis in every slow stretch. In one of them: 58 fps at the present before it, then 26, 28, 23 fps (10,462 of 10,732 MB used, 3.5 GB of it free inside the blocks).The change
Both rules read what is really allocated: the driver's count less the free space inside the blocks, as the texture budget's 90% rule already does (
gpu/vk/vram_pressure.h).RX_VK_SHADOW_RAW=1: by the count alone, as before.Numbers
Unattended runs (
HOS_HEADLESS,HOS_INPUT_SCRIPTwith the frame clock): a five-minute walk across Hyrule Field from a save in the field, DLAA 2560x1440, RTX 4070 SUPER, Ryzen 9 9950X3D. Built from4a7ae1b. The allowance was held to 9.8 GB to stand for the other programs I normally have open (a local test cap on whatvram_usagereports; it is not part of this PR). One build,RX_VK_SHADOW_RAW=1against the default, two runs each, 129 windows a run.By the count, the shadows went once or twice a run and were not made again. With this change they still went 5 and 7 times a run, each time the driver's own count passed 99% under the cap (
VRAM 9944 of 9800 MB), and were made again each time.On v2.0.11's code, before the reuse pool and the sweep of dead targets, the same comparison was wider: 56.6, 58.8 fps with 32, 8 windows under 55 by the count, against 59.8, 59.6 fps with 1, 2. At 10.4 GB allowed: 58.5, 58.6 fps with 16, 13 slow windows, against 59.3 with 6.
What losing the shadows costs in a scene that repeats exactly (standing in the field with the camera turning, 2,100-3,000 draws, the same draws in both runs; measured on v2.0.11's code),
RX_VK_WB_SHADOW_MB=0against the default:Limits
Test
python -I -B tests/test_vram_pressure.py(the rules' arithmetic with the numbers of the log above, and a source check).run_allmem_trim, texture_handle, texture_init pass.🤖 Generated with Claude Code
Thanks, the numbers make a good case, and the change itself is small and safe:
vram_usage()is justvmaGetHeapBudgets(atomic counters plus VMA's cached budget, novmaCalculateStatistics), it runs underpools_muas before, a failedcreate_device_bufferstill just means no shadow (whole copies), andRX_VK_SHADOW_RAW=1keeps the old rules. The test is registered in run_all.py and CI is green. Without VK_EXT_memory_budget,used/allowedare VMA's own block bytes and 80% of the heap, so these lines are rarely reached there. Same as before.One thing I'd like you to look at before merging. The premise is that the free space inside VMA's blocks is usable, but it isn't usable for the shadows themselves. With
preferredLargeHeapBlockSize = 64 MB(vk_vma.cpp), VMA 3.3 makes any allocation over 32 MB a dedicated allocation (size > GetPreferredBlockSize() / 2). At 272-448 MB total, most shadows are pool-sized and well over that. So each shadow raises the driver's count by its full size, and freeing it lowers the count by its full size. That has two effects:Shadows now come before textures.
shadow_fitslets the count go up to 98% and the really-allocated figure up to 93%. The texture budget squeezes at 97% count and at 90% really-allocated (vk_texture.cpp ~2911-2921). So making a shadow can now be what starts texture eviction. Evicting textures doesn't lower the count (that space stays in the blocks), so the squeeze stays on while the shadows stay. The comment onshadow_roomstill says "the textures and the mirrors come first". That may well be the right trade (your tables say the shadows are worth more than a few hundred MB of textures), but then the comment should say so. Otherwise keepshadow_fitsbelow both texture lines, e.g.used + n <= 96%and allocated+ n <= 89%. This may also be part of why the texture budget was squeezed in all four runs. #1506 (squeeze waits while blocks have room) changes the 97% side of this, so the two PRs interact.The count path has 1% hysteresis. Shadows go when the count passes 99%. Freeing them (dedicated) drops it by about S, and
shadow_fitslets them back up to 98%. That's the 5-7 go/remake cycles per run you saw. Each remake reallocates and starts withshadow_knownall zero (full fill reads). The "do not chase each other" check in the test only covers the 95/93 really-allocated pair, not 99/98. Widening the gap a bit, or adding a test case for it, would help.Smaller:
block_freeandused/allowedare summed over all device-local heaps. On AMD with ReBAR, the host-visible device-local heap's block free space counts towardallocatedeven thoughAUTO_PREFER_DEVICEshadows won't land there. The texture budget's rule already works this way, so it's a nit, not new.Nit: the long comment above
shadow_rules_rawquotes the v2.0.11 numbers (56.6/58.8 -> 59.8/59.6, 32/8 windows), not the current-base ones in the PR's first table. Either is fine, but it'd be good to say which base they're from.Nit: with the 93%/98% lines, "the textures and the mirrors come first" isn't quite true anymore (texture squeeze is at 90% allocated / 97% count). Update the comment or the lines.
@ -0,0 +1,40 @@/* How full VRAM really is, for the rules that give memory back near the driver's limit (vk_memory.cpp's write-back shadows).Shadows over 32 MB are dedicated allocations in VMA (64 MB preferred block size,
size > blockSize/2), so they never go into the block free space thatallocated()subtracts. Each one addsntouseddirectly. Withused + n <= 98%here and the texture budget squeezing atused > 97%(and at allocated > 90%, below the 93% here), making a shadow can now push the texture cache into eviction. Either keep these two lines under the texture ones (e.g. 96% / 89%), or make it explicit that shadows take priority. See the review body.@ -0,0 +22,4 @@/// at the next 1%, five to seven times in a five-minute run, each time read whole again)inline bool shadows_go(uint64_t used, uint64_t allowed, uint64_t block_free) {if (!allowed) return false;return allocated(used, block_free) > allowed / 100 * 95 || used > allowed / 100 * 99;Shadows go at a 99% count, and
shadow_fitslets them come back up to a 98% count. Since freeing dedicated shadows lowersusedby their full size, this 1% gap is what lets them cycle (the 5-7 go/remake per run in the description). Each remake starts withshadow_knowncleared. Maybe widen it, and add a 99/98 case next to the 95/93 "do not chase" check in the test.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!
Thanks, all three of my points are dealt with, and the merge conflict is gone (it merges cleanly into main now).
Fixed:
shadow_fitsis now 90% really allocated / 97% count, so a shadow is only made where the textures' own lines (90% allocated, 97% count invk_texture.cpp) are not passed. The comment onshadow_roomis true again, and the runs you added tovram_pressure.h(96/89 never got the shadows back, 90/97 did) are a good reason for not going lower.shadow_rules_rawnow says it is v2.0.11's code.#1821 (compaction, merged): I read it against this. It does not touch
vk_memory.cpp, and the two compose. Compact is opt-in; without it nothing changes here. With it the texture budget holds the count near 85%, so your 97% count line does not bind and the shadows are only judged by the 90% allocated line. One small mismatch, a nit and not a blocker: with compaction the texture budget takes only the textures' pool's free space off the count (vram_compact::reclaimable), whileallocated()here subtracts the free space of all blocks. So a shadow can be made where the texture side, which sees less free space, is a little more pressed. With the hold at 85% there are roughly 5 points of room before the textures' 90% line, so I would not expect it to matter for 272-448 MB; just worth knowing if someone sees shadows and texture eviction trading off withRX_VK_COMPACT=1.CI: the shadows test passes on both Linux and Windows. The two failing checks (
nx2/test_draw_parameters.py,tests/test_antilag.py; alsotest_wayland_libdecor_hinton Windows) are in code this PR does not touch, from a base that was moving that day. Please re-run after the next merge of main to be sure. Not tested on my side: I did not build or run it.