perf(vk): the 90% rule and the driver's limit evict their excess, not an eighth of the texture cache #2826

Closed
github-sync wants to merge 1 commit from github/pr/2826 into main
Owner

@klikso opened this pull request on GitHub · 2026-11-12 15:34 UTC · original
Merged on GitHub in ba3d3f73ec

Why. On an 8 GB card the texture cache sits right at the 90% line in the larger scenes. When it is a few MB over, the pass lets go of an eighth of the cache, textures that were on screen a second ago, and they are made again within the camera's turn: a loop of evictions, uploads and compaction runs that shows as a long frame every second or two.

  • rule_target cuts to 7/8 of the rules' budget once the textures hold more than it. That is right for a budget that is a size (the card's or a preset's).
  • The 90% rule's budget and the driver-limit (97%) rule's are not sizes: each is "what the textures hold, less the excess over a line". The comment on the hold's own pass already says so for the hold ("what goes is the excess alone ... crossed again whenever anything else grows", the 4,589 -> 2,312 MB case), and the hold was changed for it. These two rules still went through 7/8.
  • Measured: Castle Town Market entered from Hyrule Field, RTX 3060 Ti 8 GB. 2,873 MB of textures, 62 MB over the 90% line. The pass's target was 7/8 x 2,811 = 2,460 MB: ~400 MB of textures unused for 60 presents go, all in view again a few seconds later. The log shows it as a sawtooth, textures retired ... budget 386 / 362 / 295 and the cache going 2.95 -> 2.5 -> 2.95 GB, with 8-9 uploads a frame, where the same scene loaded from a save (just under the line) has 0.

What

  • vram_compact::rule_target(total, size_budget, rule_budget): the budget's size still asks for its 7/8. A rule budget below the size (the 90% rule, the driver's limit) asks for its excess and kExcessSlack (64 MB) more, so that not every pass is one. The lower of the two when both apply.
  • texture_frame keeps the budget as it was before the 90% rule (size_budget) and passes both.
  • RX_VK_EVICT_EXCESS=0: to 7/8 of their budget, as before.
  • Only the compacted path (RX_VK_COMPACT, the default) goes through rule_target; with the compaction off nothing changes.

Measured

Note: every run below had this PC's DLSS model row on Transformer 2 (L), not the default K: the harder case (more GPU time and VRAM, #2836). With K the gain is smaller; those numbers are in the comment below.

Windows 11, RTX 3060 Ti 8 GB (driver 32.0.16.2114), Ryzen 5 5600X, 2560x1440 DLSS. main ba005ca4 with #2711, #2731 (at bce3d17e, where it was still on by default), #2685 and #2709 merged locally. Load the save, teleport to Hyrule Field, 10 s there, teleport to the Market, settle, then 80 s of the camera turning. RX_VK_FRAMEGEN=0, HOS_FPS_MAX=240. Three runs an arm, arms alternated. The other half of the same stutter is #2825 (the previous scene's oversized mirrors), so both are shown alone and together.

arm fps mean frames over 50 ms in 80 s after the first 20 s: fps after the first 20 s: frames over 50 ms per 20 s
main stack 44.4 / 45.0 / 45.0 100 / 69 / 60 46.5 12.3
this PR 43.3 / 46.6 / 46.2 75 / 25 / 60 47.0 7.0
#2825 alone 45.7 / 47.2 / 47.4 66 / 37 / 28 48.5 4.7
this PR + #2825 49.7 / 48.8 / 49.3 10 / 15 / 22 50.6 0.3
  • Alone it is a modest gain and noisy (one of its three runs was the slowest of all twelve in its first 40 s). The clear result is the pair: with the mirrors trimmed the cache is close enough to the line that taking the excess alone ends the loop, 0 or 1 frame over 50 ms in each 20 s after the arrival, in all three runs.
  • Loaded from a save in the Market, two 30 s runs an arm: 47.1 / 47.9 fps and 4 / 1 frames over 50 ms with this and #2825 on, 46.5 / 46.0 and 6 / 7 with both off; 0-1 uploads a frame against 2-7. (In the morning the same load was 53-55 fps and under the line; by these runs the driver's allowance was ~100 MB lower, 6,944 against 7,043 MB, which puts it over the line as well. No trim runs in a direct load, so this difference is this PR's.)

Not tested

  • AMD, Intel, Linux; cards of 4-6 GB, where the count sits past 90% all the time and the excess is larger. The rule there lets go of the same excess as before plus 64 MB instead of an eighth, so the count stays nearer the line; if a card needs the deeper cut to stay clear of the driver's limit, the 97% rule and the hold are unchanged and still take theirs. That is reasoning, not a measurement: a result from a 6 GB card would be welcome.
  • With the compaction off.

Tests

  • tests/test_vram_compact.cpp: the three-argument rule_target (no rule, the excess alone with the Market's numbers, the size's 7/8, both, a budget under the slack), next to the existing cases, which are unchanged.
  • tests/test_vram_compact.py checks texture_frame keeps size_budget and calls it, with the switch.
  • python -I -B tests/run_all.py vram_compact texture_squeeze vram_pressure vram_budget preset_texture target_image (the checks that read this code): 6 passed. The full suite was run on #2825's branch (460 passed; the 2 that fail there fail on main here too), not again on this one.

🤖 Generated with Claude Code

> [@klikso](https://github.com/klikso) opened this pull request on GitHub · 2026-11-12 15:34 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826) > **Merged on GitHub** in https://git.magicbean.dev/MagicBeanDev/Hookshot/commit/ba3d3f73ec58c6450e52946143c9e53b6d7aa5ec **Why.** On an 8 GB card the texture cache sits right at the 90% line in the larger scenes. When it is a few MB over, the pass lets go of an eighth of the cache, textures that were on screen a second ago, and they are made again within the camera's turn: a loop of evictions, uploads and compaction runs that shows as a long frame every second or two. - `rule_target` cuts to 7/8 of the rules' budget once the textures hold more than it. That is right for a budget that is a size (the card's or a preset's). - The 90% rule's budget and the driver-limit (97%) rule's are not sizes: each is "what the textures hold, less the excess over a line". The comment on the hold's own pass already says so for the hold ("what goes is the excess alone ... crossed again whenever anything else grows", the 4,589 -> 2,312 MB case), and the hold was changed for it. These two rules still went through 7/8. - Measured: Castle Town Market entered from Hyrule Field, RTX 3060 Ti 8 GB. `2,873 MB` of textures, 62 MB over the 90% line. The pass's target was `7/8 x 2,811 = 2,460 MB`: ~400 MB of textures unused for 60 presents go, all in view again a few seconds later. The log shows it as a sawtooth, `textures retired ... budget 386 / 362 / 295` and the cache going 2.95 -> 2.5 -> 2.95 GB, with 8-9 uploads a frame, where the same scene loaded from a save (just under the line) has 0. ## What - `vram_compact::rule_target(total, size_budget, rule_budget)`: the budget's size still asks for its 7/8. A rule budget below the size (the 90% rule, the driver's limit) asks for its excess and `kExcessSlack` (64 MB) more, so that not every pass is one. The lower of the two when both apply. - `texture_frame` keeps the budget as it was before the 90% rule (`size_budget`) and passes both. - `RX_VK_EVICT_EXCESS=0`: to 7/8 of their budget, as before. - Only the compacted path (`RX_VK_COMPACT`, the default) goes through `rule_target`; with the compaction off nothing changes. ## Measured **Note:** every run below had this PC's DLSS model row on Transformer 2 (L), not the default K: the harder case (more GPU time and VRAM, #2836). With K the gain is smaller; those numbers are in the comment below. Windows 11, RTX 3060 Ti 8 GB (driver 32.0.16.2114), Ryzen 5 5600X, 2560x1440 DLSS. `main` ba005ca4 with #2711, #2731 (at bce3d17e, where it was still on by default), #2685 and #2709 merged locally. Load the save, teleport to Hyrule Field, 10 s there, teleport to the Market, settle, then **80 s** of the camera turning. `RX_VK_FRAMEGEN=0`, `HOS_FPS_MAX=240`. Three runs an arm, arms alternated. The other half of the same stutter is #2825 (the previous scene's oversized mirrors), so both are shown alone and together. | arm | fps mean | frames over 50 ms in 80 s | after the first 20 s: fps | after the first 20 s: frames over 50 ms per 20 s | |---|---|---|---|---| | main stack | 44.4 / 45.0 / 45.0 | 100 / 69 / 60 | 46.5 | 12.3 | | **this PR** | 43.3 / 46.6 / 46.2 | 75 / 25 / 60 | 47.0 | 7.0 | | #2825 alone | 45.7 / 47.2 / 47.4 | 66 / 37 / 28 | 48.5 | 4.7 | | this PR + #2825 | 49.7 / 48.8 / 49.3 | 10 / 15 / 22 | 50.6 | 0.3 | - Alone it is a modest gain and noisy (one of its three runs was the slowest of all twelve in its first 40 s). The clear result is the pair: with the mirrors trimmed the cache is close enough to the line that taking the excess alone ends the loop, 0 or 1 frame over 50 ms in each 20 s after the arrival, in all three runs. - Loaded from a save in the Market, two 30 s runs an arm: 47.1 / 47.9 fps and 4 / 1 frames over 50 ms with this and #2825 on, 46.5 / 46.0 and 6 / 7 with both off; 0-1 uploads a frame against 2-7. (In the morning the same load was 53-55 fps and under the line; by these runs the driver's allowance was ~100 MB lower, 6,944 against 7,043 MB, which puts it over the line as well. No trim runs in a direct load, so this difference is this PR's.) ## Not tested - AMD, Intel, Linux; cards of 4-6 GB, where the count sits past 90% all the time and the excess is larger. The rule there lets go of the same excess as before plus 64 MB instead of an eighth, so the count stays nearer the line; if a card needs the deeper cut to stay clear of the driver's limit, the 97% rule and the hold are unchanged and still take theirs. That is reasoning, not a measurement: a result from a 6 GB card would be welcome. - With the compaction off. ## Tests - `tests/test_vram_compact.cpp`: the three-argument `rule_target` (no rule, the excess alone with the Market's numbers, the size's 7/8, both, a budget under the slack), next to the existing cases, which are unchanged. - `tests/test_vram_compact.py` checks `texture_frame` keeps `size_budget` and calls it, with the switch. - `python -I -B tests/run_all.py vram_compact texture_squeeze vram_pressure vram_budget preset_texture target_image` (the checks that read this code): 6 passed. The full suite was run on #2825's branch (460 passed; the 2 that fail there fail on `main` here too), not again on this one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- github-sync:item:2826 -->
rule_target cut to 7/8 of the rules' budget once the textures held more than it. That is right for
a budget that is a size. The 90% rule's and the driver-limit rule's budgets are "what the textures
hold less the excess over a line", as the hold's is, and are crossed again whenever anything grows:
Castle Town Market entered from Hyrule Field on an 8 GB card sat 62 MB over the 90% line and lost
~400 MB of textures unused for a second, all in view again within the camera's turn, made again,
over the line again. For those two rules what goes is the excess and 64 MB more; the budget's size
still asks for its 7/8. RX_VK_EVICT_EXCESS=0: as before.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Owner

@klikso commented on GitHub · 2026-11-12 16:09 UTC · original

A correction to the measurements above, found an hour after opening this: this PC's DLSS model row was set to Transformer 2 (L), not the default (K), for every run in the description. L costs ~4 ms more GPU time a frame on this card (#2836) and, judging by the count, more VRAM too, so it is the harder case. The same test with the default model (RX_VK_DLSS_PRESET=K, everything else as in the description, 80 s, two runs an arm, alternated; "both" is #2825 + #2826):

arm (DLSS K) fps mean frames over 50 ms in 80 s fps by 20 s quarter
main stack 50.4 / 52.0 10 / 8 47.7 50.8 49.7 53.9 / 51.7 50.0 53.2 53.4
both 50.6 / 54.2 6 / 1 45.5 48.6 53.4 56.3 / 51.8 54.4 56.3 54.7
  • With K the Market entered from the field is much less short of memory to begin with (8-10 long frames in 80 s on the main stack, against 60-100 with L), so there is less for these two changes to win: about 1 fps and 4-9 fewer long frames, on two runs an arm, which is within what two runs can show.
  • The mechanism is the same in both (the trims happen, the rule's cut is the excess), and the description's numbers stand for a card that is further over the line: this one with L, or a card with less VRAM. I have not run the two changes apart with K.
  • I'll use K for anything further here, and say so.
> [@klikso](https://github.com/klikso) commented on GitHub · 2026-11-12 16:09 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#issuecomment-6828916704) A correction to the measurements above, found an hour after opening this: **this PC's DLSS model row was set to Transformer 2 (L), not the default (K)**, for every run in the description. L costs ~4 ms more GPU time a frame on this card (#2836) and, judging by the count, more VRAM too, so it is the harder case. The same test with the default model (`RX_VK_DLSS_PRESET=K`, everything else as in the description, 80 s, two runs an arm, alternated; "both" is #2825 + #2826): | arm (DLSS K) | fps mean | frames over 50 ms in 80 s | fps by 20 s quarter | |---|---|---|---| | main stack | 50.4 / 52.0 | 10 / 8 | 47.7 50.8 49.7 53.9 / 51.7 50.0 53.2 53.4 | | both | 50.6 / 54.2 | 6 / 1 | 45.5 48.6 53.4 56.3 / 51.8 54.4 56.3 54.7 | - With K the Market entered from the field is much less short of memory to begin with (8-10 long frames in 80 s on the main stack, against 60-100 with L), so there is less for these two changes to win: about 1 fps and 4-9 fewer long frames, on two runs an arm, which is within what two runs can show. - The mechanism is the same in both (the trims happen, the rule's cut is the excess), and the description's numbers stand for a card that is further over the line: this one with L, or a card with less VRAM. I have not run the two changes apart with K. - I'll use K for anything further here, and say so. <!-- github-sync:c:6828916704 -->
Author
Owner

@hckrman101 commented on gpu/vk/vram_compact.h line 82 on GitHub · 2026-11-12 16:34 UTC · original

rule_budget here is the lower of the 90% rule's and the 97% rule's budgets, so the driver's limit gets the 64 MB slack too (see the review body). To keep the deeper cut at the limit, texture_frame could pass the 90% rule's budget here and keep the old two-argument form for the 97% part, taking the lower of the two.

> [@hckrman101](https://github.com/hckrman101) commented on `gpu/vk/vram_compact.h` line 82 on GitHub · 2026-11-12 16:34 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#discussion_r4553300643) `rule_budget` here is the lower of the 90% rule's and the 97% rule's budgets, so the driver's limit gets the 64 MB slack too (see the review body). To keep the deeper cut at the limit, `texture_frame` could pass the 90% rule's budget here and keep the old two-argument form for the 97% part, taking the lower of the two. <!-- github-sync:rc:4553300643 -->
Author
Owner

@hckrman101 commented on gpu/vk/vk_texture.cpp line 4305 on GitHub · 2026-11-12 16:34 UTC · original

This is the 97% rule's contribution to rule_budget, which now also goes through the excess-only target. #2711 changes the condition above, so expect a small rebase.

> [@hckrman101](https://github.com/hckrman101) commented on `gpu/vk/vk_texture.cpp` line 4305 on GitHub · 2026-11-12 16:34 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#discussion_r4553300662) This is the 97% rule's contribution to `rule_budget`, which now also goes through the excess-only target. #2711 changes the condition above, so expect a small rebase. <!-- github-sync:rc:4553300662 -->
Author
Owner

@hckrman101 reviewed on GitHub · 2026-11-12 16:34 UTC · original

Thanks, the diagnosis reads right to me, and the change is small and contained. The 90% rule's budget is "total less the excess", not a size, so a 7/8 cut of it is ~an eighth of the cache for a few MB over the line. The hold was already fixed the same way. I traced it through texture_frame and found no correctness problems:

  • Units are consistent. total_bytes counts allocation size (bytes + pad), and hold_met counts the chosen textures the same way. The excess comes from the driver's count less the textures' pool free space, and an evicted texture's allocation goes straight into that free space. So a pass that takes excess + 64 MB really does bring the 90% rule's pressure 64 MB under the line by the next budget pass, ten presents later, once dead has been freed.
  • No new ratchet or thrash. rule_to is only used in the hold's pass, for textures unused 1-10 s. Older ones still go to the 85/80 lines. Each pass is LRU and bounded, and the next one needs 64 MB of new pressure. Where the scene's working set is really bigger than the room under the line, you'll see more small passes instead of fewer big ones. That's an improvement: re-uploads can only go down when less goes per pass. One thing it doesn't fix: an evicted texture whose memory isn't in the textures' pool doesn't lower the count, so the next pass sees the same excess. That happened with the 7/8 cut too, only bigger, and #2711 handles that case for the 97% rule.
  • Compaction off is unchanged, as the description says (that pass still breaks at budget / 8 * 7).

One thing the description gets wrong, and it matters on small cards. "Not tested" says the 97% rule is unchanged and still takes its share. With the compaction on, the 97% rule's cut now always goes through rule_target as excess + 64 MB too. It only fires while the blocks hold less than an eighth of the allowance free, so the reclaim is always under the 17 points that would put its budget below the reserve's. That makes evict_to non-zero and the pass is the hold's. The title says this, but the reasoning about 4-6 GB cards doesn't. At 97% the driver is about to page VRAM out, which costs far more than re-uploading a texture, so the eighth of headroom was doing something there. I'd either keep the 7/8 cut for the driver's limit and take only the 90% rule's excess (track the 90% rule's budget apart from the 97% one), or show it holds on a smaller allowance. RX_VK_TEST_ALLOWED_MB=5632 on the 3060 Ti gives a 6 GB card's allowance, and a few minutes of the field and the Market with the 97% line's log lines would do.

On the numbers (thanks for posting the correction quickly): with the default DLSS model (K), the only measurement is #2825 + #2826 against the main stack, two runs each, and that's within noise. Since this one changes the default eviction for every card, could you add the pair that isolates it with K: #2825 alone vs #2825 + this PR, same Market-from-the-field route? Please include the count the description already uses: uploads a frame and the textures retired sawtooth. That shows the mechanism much more clearly than fps does. A base without #2731's old default-on state would also match what will land.

CI is green and the branch merges cleanly. It will need a small rebase after #2711, which edits the 97% rule's condition three lines above your hunk; the two are complementary. #2834 touches vram_compact.h and its tests in other places.

> [@hckrman101](https://github.com/hckrman101) reviewed on GitHub · 2026-11-12 16:34 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#pullrequestreview-5878229980) Thanks, the diagnosis reads right to me, and the change is small and contained. The 90% rule's budget is "total less the excess", not a size, so a 7/8 cut of it is ~an eighth of the cache for a few MB over the line. The hold was already fixed the same way. I traced it through `texture_frame` and found no correctness problems: - **Units are consistent.** `total_bytes` counts allocation size (`bytes + pad`), and `hold_met` counts the chosen textures the same way. The excess comes from the driver's count less the textures' pool free space, and an evicted texture's allocation goes straight into that free space. So a pass that takes excess + 64 MB really does bring the 90% rule's pressure 64 MB under the line by the next budget pass, ten presents later, once `dead` has been freed. - **No new ratchet or thrash.** `rule_to` is only used in the hold's pass, for textures unused 1-10 s. Older ones still go to the 85/80 lines. Each pass is LRU and bounded, and the next one needs 64 MB of new pressure. Where the scene's working set is really bigger than the room under the line, you'll see more small passes instead of fewer big ones. That's an improvement: re-uploads can only go down when less goes per pass. One thing it doesn't fix: an evicted texture whose memory isn't in the textures' pool doesn't lower the count, so the next pass sees the same excess. That happened with the 7/8 cut too, only bigger, and #2711 handles that case for the 97% rule. - Compaction off is unchanged, as the description says (that pass still breaks at `budget / 8 * 7`). One thing the description gets wrong, and it matters on small cards. "Not tested" says the 97% rule is unchanged and still takes its share. With the compaction on, the 97% rule's cut now always goes through `rule_target` as excess + 64 MB too. It only fires while the blocks hold less than an eighth of the allowance free, so the reclaim is always under the 17 points that would put its budget below the reserve's. That makes `evict_to` non-zero and the pass is the hold's. The title says this, but the reasoning about 4-6 GB cards doesn't. At 97% the driver is about to page VRAM out, which costs far more than re-uploading a texture, so the eighth of headroom was doing something there. I'd either keep the 7/8 cut for the driver's limit and take only the 90% rule's excess (track the 90% rule's budget apart from the 97% one), or show it holds on a smaller allowance. `RX_VK_TEST_ALLOWED_MB=5632` on the 3060 Ti gives a 6 GB card's allowance, and a few minutes of the field and the Market with the 97% line's log lines would do. On the numbers (thanks for posting the correction quickly): with the default DLSS model (K), the only measurement is #2825 + #2826 against the main stack, two runs each, and that's within noise. Since this one changes the default eviction for every card, could you add the pair that isolates it with K: #2825 alone vs #2825 + this PR, same Market-from-the-field route? Please include the count the description already uses: uploads a frame and the `textures retired` sawtooth. That shows the mechanism much more clearly than fps does. A base without #2731's old default-on state would also match what will land. CI is green and the branch merges cleanly. It will need a small rebase after #2711, which edits the 97% rule's condition three lines above your hunk; the two are complementary. #2834 touches `vram_compact.h` and its tests in other places. <!-- github-sync:r:5878229980 -->
Author
Owner

@hclaria commented on GitHub · 2026-11-12 16:36 UTC · original

Edit: a same-route A/B shows a much smaller effect than the play-session numbers below (6% fewer textures let go, the same fps). See the correction.

Data from a Steam Deck, where the budget's size is what evicts: the same sawtooth, and the same remedy seems to work there

This PR is not in my renderer, but reading it explained what I see on a Steam Deck. Here are the numbers, in case they help for the case the PR leaves at 7/8: a budget that is a size.

Setup. Steam Deck OLED (Van Gogh 0x1435, 16 GB), SteamOS 3.9.5, kernel 7.2.7-valve2, Mesa 26.2.0-devel (RADV). Game built from main at c544828f. Renderer: c544828f with #2789, #2644, #2774, #2731, #2767 and #2649 merged, plus the readback test patch of this comment, in both sessions below. RX_VK_VRAM_MB=2000, RX_VK_TEX_MAX_MB=128, RX_VK_TEX_MAX_DIM=1024, RX_VK_LOW_SPEC=1.

Which rule evicts here. The driver's count stays under the 90% line (VRAM: 4743 MB used of 5977 MB allowed (1647 MB of it free in the blocks); textures 1976 MB (budget 2000 MB)), so what evicts is the budget's size, and the cuts land at 1,742 to 1,750 MB: budget / 8 * 7. The budget is that low because on this APU the textures are system RAM, and RAM is what runs out.

As it is, in play (13 minutes): 170 passes for the budget, 16,485 textures let go (1,250 a minute); 51 a frame's uploads took ... lines, 5.9 s in all.

With the excess and 64 MB instead (7 lines, below: RX_VK_EVICT_SLACK_MB=64 makes the plain pass stop at the budget less 64 MB), 10 minutes in play: 68 passes, 6,811 textures (663 a minute), the cache never under 1,920 MB after a pass; 11 such upload lines, 1.0 s in all.

draws a frame 7/8 (windows of 120 presents) excess + 64 MB (windows)
1,000 to 2,000 28.5 fps (116) 26.0 fps (68)
2,000 to 2,700 19.2 fps (22) 20.4 fps (32)
over 2,700 15.7 fps (11) 20.7 fps (18)

Limits. One Deck, one session each, two different routes. A system memory setting also changed between the two sessions (MemoryLow 8G to 6G, the edit at the top of this comment), which probably accounts for part of the fps difference in the heaviest scenes. The eviction counts and the upload lines are the numbers I trust; the fps table is there for completeness.

So where the budget is a size well under what the driver allows, cutting to 7/8 of it seems to cost what this PR removes for the 90% rule. Whether that belongs here, in a follow-up or behind a switch is yours to judge. I am glad to run a build on this Deck and report.

The test patch (gpu/vk/vk_texture.cpp, on main c544828f with the six PRs above)
diff --git a/gpu/vk/vk_texture.cpp b/gpu/vk/vk_texture.cpp
index 7dc58baf..7ce7c1f4 100644
--- a/gpu/vk/vk_texture.cpp
+++ b/gpu/vk/vk_texture.cpp
@@ -4527,10 +4527,16 @@ void texture_frame(uint64_t completed) {
             if (frame_no - last < need_unused && !shifted) continue;
             if (shifted || drawn_over(tex)) hold_dead += tex->bytes + tex->pad;
         }
+    // (deck test) RX_VK_EVICT_SLACK_MB=<MB>: a pass over the budget's size lets go of the excess and that much more, not an
+    // eighth of the budget. A Steam Deck at RX_VK_VRAM_MB=2000: 19,351 textures went for the budget in 14 minutes of play
+    // (203 passes, the cache cut from 2,000 to 1,750 MB each time), what was on screen seconds before among them
+    static const int64_t evict_slack_mb = getenv("RX_VK_EVICT_SLACK_MB") ? atoll(getenv("RX_VK_EVICT_SLACK_MB")) : -1;
+    const uint64_t cut_to = evict_slack_mb < 0 ? budget / 8 * 7
+                            : std::max(budget / 8 * 7, budget - std::min(budget, (uint64_t)evict_slack_mb << 20));
     for (auto& [last, tex] : order) {
         // (total_bytes can have drifted below the textures' sum (destroy_later clamps it): the chosen ones' bytes past it mean the
         // budget is met, not a wrapped difference that never is)
-        if ((!sweep && !by_hold && (pending_bytes >= total_bytes || total_bytes - pending_bytes <= budget / 8 * 7)) ||
+        if ((!sweep && !by_hold && (pending_bytes >= total_bytes || total_bytes - pending_bytes <= cut_to)) ||
             frame_no - last < need_dead)
             break;
         const bool shifted = drawn_over_shifted(tex);
> [@hclaria](https://github.com/hclaria) commented on GitHub · 2026-11-12 16:36 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#issuecomment-6829633476) **Edit: a same-route A/B shows a much smaller effect than the play-session numbers below (6% fewer textures let go, the same fps). See [the correction](https://git.magicbean.dev/MagicBeanDev/Hookshot/pulls/2826).** **Data from a Steam Deck, where the budget's size is what evicts: the same sawtooth, and the same remedy seems to work there** This PR is not in my renderer, but reading it explained what I see on a Steam Deck. Here are the numbers, in case they help for the case the PR leaves at 7/8: a budget that is a size. **Setup.** Steam Deck OLED (Van Gogh `0x1435`, 16 GB), SteamOS 3.9.5, kernel 7.2.7-valve2, Mesa 26.2.0-devel (RADV). Game built from main at `c544828f`. Renderer: `c544828f` with #2789, #2644, #2774, #2731, #2767 and #2649 merged, plus the readback test patch of [this comment](https://git.magicbean.dev/MagicBeanDev/Hookshot/issues/1000), in both sessions below. `RX_VK_VRAM_MB=2000`, `RX_VK_TEX_MAX_MB=128`, `RX_VK_TEX_MAX_DIM=1024`, `RX_VK_LOW_SPEC=1`. **Which rule evicts here.** The driver's count stays under the 90% line (`VRAM: 4743 MB used of 5977 MB allowed (1647 MB of it free in the blocks); textures 1976 MB (budget 2000 MB)`), so what evicts is the budget's size, and the cuts land at 1,742 to 1,750 MB: `budget / 8 * 7`. The budget is that low because on this APU the textures are system RAM, and RAM is what runs out. **As it is, in play** (13 minutes): 170 passes for the budget, 16,485 textures let go (1,250 a minute); 51 `a frame's uploads took ...` lines, 5.9 s in all. **With the excess and 64 MB instead** (7 lines, below: `RX_VK_EVICT_SLACK_MB=64` makes the plain pass stop at the budget less 64 MB), 10 minutes in play: 68 passes, 6,811 textures (663 a minute), the cache never under 1,920 MB after a pass; 11 such upload lines, 1.0 s in all. | draws a frame | 7/8 (windows of 120 presents) | excess + 64 MB (windows) | |---|---|---| | 1,000 to 2,000 | 28.5 fps (116) | 26.0 fps (68) | | 2,000 to 2,700 | 19.2 fps (22) | 20.4 fps (32) | | over 2,700 | 15.7 fps (11) | 20.7 fps (18) | **Limits.** One Deck, one session each, two different routes. A system memory setting also changed between the two sessions (`MemoryLow` 8G to 6G, the edit at the top of [this comment](https://git.magicbean.dev/MagicBeanDev/Hookshot/issues/1000)), which probably accounts for part of the fps difference in the heaviest scenes. The eviction counts and the upload lines are the numbers I trust; the fps table is there for completeness. So where the budget is a size well under what the driver allows, cutting to 7/8 of it seems to cost what this PR removes for the 90% rule. Whether that belongs here, in a follow-up or behind a switch is yours to judge. I am glad to run a build on this Deck and report. <details><summary>The test patch (gpu/vk/vk_texture.cpp, on main c544828f with the six PRs above)</summary> ```diff diff --git a/gpu/vk/vk_texture.cpp b/gpu/vk/vk_texture.cpp index 7dc58baf..7ce7c1f4 100644 --- a/gpu/vk/vk_texture.cpp +++ b/gpu/vk/vk_texture.cpp @@ -4527,10 +4527,16 @@ void texture_frame(uint64_t completed) { if (frame_no - last < need_unused && !shifted) continue; if (shifted || drawn_over(tex)) hold_dead += tex->bytes + tex->pad; } + // (deck test) RX_VK_EVICT_SLACK_MB=<MB>: a pass over the budget's size lets go of the excess and that much more, not an + // eighth of the budget. A Steam Deck at RX_VK_VRAM_MB=2000: 19,351 textures went for the budget in 14 minutes of play + // (203 passes, the cache cut from 2,000 to 1,750 MB each time), what was on screen seconds before among them + static const int64_t evict_slack_mb = getenv("RX_VK_EVICT_SLACK_MB") ? atoll(getenv("RX_VK_EVICT_SLACK_MB")) : -1; + const uint64_t cut_to = evict_slack_mb < 0 ? budget / 8 * 7 + : std::max(budget / 8 * 7, budget - std::min(budget, (uint64_t)evict_slack_mb << 20)); for (auto& [last, tex] : order) { // (total_bytes can have drifted below the textures' sum (destroy_later clamps it): the chosen ones' bytes past it mean the // budget is met, not a wrapped difference that never is) - if ((!sweep && !by_hold && (pending_bytes >= total_bytes || total_bytes - pending_bytes <= budget / 8 * 7)) || + if ((!sweep && !by_hold && (pending_bytes >= total_bytes || total_bytes - pending_bytes <= cut_to)) || frame_no - last < need_dead) break; const bool shifted = drawn_over_shifted(tex); ``` </details> <!-- github-sync:c:6829633476 -->
Author
Owner

@hckrman101 approved on GitHub · 2026-11-12 16:56 UTC · original

I accept this as is, including the 97% path now taking the excess plus 64 MB too. Approved as the maintainer.

Not a blocker, but please keep an eye on 4-6 GB cards. If you can, post a run with RX_VK_TEST_ALLOWED_MB=5632 (the field and the Market, with the 97% line's log lines). This one goes together with #2825. #2711 edits the 97% condition three lines above your hunk, so whichever lands second may need a small rebase.

> [@hckrman101](https://github.com/hckrman101) approved on GitHub · 2026-11-12 16:56 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#pullrequestreview-5878563304) I accept this as is, including the 97% path now taking the excess plus 64 MB too. Approved as the maintainer. Not a blocker, but please keep an eye on 4-6 GB cards. If you can, post a run with `RX_VK_TEST_ALLOWED_MB=5632` (the field and the Market, with the 97% line's log lines). This one goes together with #2825. #2711 edits the 97% condition three lines above your hunk, so whichever lands second may need a small rebase. <!-- github-sync:r:5878563304 -->
github-sync closed this pull request 2026-11-12 17:59:39 +00:00
Author
Owner

@klikso commented on GitHub · 2026-11-12 17:03 UTC · original

Thanks for the review and the merge. Two notes for the record:

  • The 97% path. I had made the change you suggested as the first option (only the 90% rule takes its excess, the driver's limit keeps its 7/8 through the two-argument form) and pushed it as 85ba2fd7 to this branch a few minutes after the merge, so it is not in main. I'll run the 6 GB model you asked for (RX_VK_TEST_ALLOWED_MB=5632, the field and the Market, with the 97% line's lines) on main as merged, and open that commit as a follow-up only if the run shows the limit needs its eighth.
  • K-model runs for this PR and #2825 apart are running now on main at bd9b07e6 (the arms by RX_VK_MIRROR_TRIM / RX_VK_EVICT_EXCESS); I'll post them here and on #2825 with the uploads and the textures retired counts.

@hclaria thank you for the Deck numbers: 1,250 -> 663 textures a minute and 5.9 -> 1.0 s of upload stalls is the same sawtooth one rule over. That case (a budget that is a size, well under what the driver allows) is not touched by this PR, and your seven-line patch is the natural follow-up. It is yours to open if you want it; I only have this one NVIDIA card, so I could not measure it where it matters.

> [@klikso](https://github.com/klikso) commented on GitHub · 2026-11-12 17:03 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#issuecomment-6830327947) Thanks for the review and the merge. Two notes for the record: - **The 97% path.** I had made the change you suggested as the first option (only the 90% rule takes its excess, the driver's limit keeps its 7/8 through the two-argument form) and pushed it as `85ba2fd7` to this branch a few minutes after the merge, so it is not in main. I'll run the 6 GB model you asked for (`RX_VK_TEST_ALLOWED_MB=5632`, the field and the Market, with the 97% line's lines) on main as merged, and open that commit as a follow-up only if the run shows the limit needs its eighth. - **K-model runs** for this PR and #2825 apart are running now on main at `bd9b07e6` (the arms by `RX_VK_MIRROR_TRIM` / `RX_VK_EVICT_EXCESS`); I'll post them here and on #2825 with the uploads and the `textures retired` counts. @hclaria thank you for the Deck numbers: 1,250 -> 663 textures a minute and 5.9 -> 1.0 s of upload stalls is the same sawtooth one rule over. That case (a budget that is a size, well under what the driver allows) is not touched by this PR, and your seven-line patch is the natural follow-up. It is yours to open if you want it; I only have this one NVIDIA card, so I could not measure it where it matters. <!-- github-sync:c:6830327947 -->
Author
Owner

@klikso commented on GitHub · 2026-11-12 17:30 UTC · original

The K-model runs for this PR and #2825 apart, on main as merged, are in this comment on #2825: on top of #2825 this one takes the textures let go for the budget from ~10,700 to ~4,900 in 80 s, the uploads from 2.7 to 1.5 a frame and the replay's submit from 12.2 to 11.4 ms, at the same frame rate within three runs. The 6 GB model run is still to come.

> [@klikso](https://github.com/klikso) commented on GitHub · 2026-11-12 17:30 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#issuecomment-6830995126) The K-model runs for this PR and #2825 apart, on main as merged, are in [this comment on #2825](https://git.magicbean.dev/MagicBeanDev/Hookshot/pulls/2825): on top of #2825 this one takes the textures let go for the budget from ~10,700 to ~4,900 in 80 s, the uploads from 2.7 to 1.5 a frame and the replay's submit from 12.2 to 11.4 ms, at the same frame rate within three runs. The 6 GB model run is still to come. <!-- github-sync:c:6830995126 -->
Author
Owner

@hclaria commented on GitHub · 2026-11-12 18:21 UTC · original

Correction to my numbers above: on the same route the effect is small, and I am not opening a follow-up for it

I ran the A/B the earlier comment lacked, and it does not hold up the play-session figures.

Setup. Steam Deck OLED (Van Gogh 0x1435, 16 GB), SteamOS 3.9.5, Mesa 26.2.0-devel (RADV). Game built from main at c544828f. Renderer: main at 284c9121 (it has this PR) plus the size-budget change, one binary for both arms, RX_VK_EVICT_SIZE_EXCESS=0 against the default. RX_VK_VRAM_MB=2000, RX_VK_LOW_SPEC=1. Headless, a save in play, Link standing still and the camera turning the same way for the whole run, 240 s measured, runs alternated.

7/8 (run 1) 7/8 (run 2) excess + 64 MB (run 1) excess + 64 MB (run 2)
fps 23.23 23.32 22.99 23.19
draws a frame 1,262 1,262 1,275 1,283
textures let go for the budget 7,637 7,510 6,916 7,307
passes for the budget 137 120 196 211
uploads a frame 1.50 (0.89 MB) 1.50 (0.86 MB) 1.38 (0.78 MB) 1.47 (0.83 MB)
a frame's uploads took lines 5, 366 ms 5, 453 ms 1, 104 ms 2, 147 ms
lowest window of 120 presents 21.9 fps 20.1 fps 14.4 fps 17.9 fps
frame time, standard deviation 1.52 ms 2.04 ms 4.94 ms 3.03 ms

So: 6% fewer textures let go, 5% fewer uploads, fewer long upload frames, the same fps within the noise, and a wider spread of frame times with the change (more passes, each with its own work).

What was wrong with the earlier numbers. The two play sessions were not the same route, and a system memory setting changed between them. A first scripted A/B with Link walking had the same fault: the two arms ended up looking at different views (1,320 against 1,961 draws), and two runs of the same arm differed by a third in textures let go (11,696 and 8,757). Only the turning camera gave runs that repeat.

Why the cut matters so little here. Over a full turn this scene uses more textures than the 2,000 MB budget holds, so the budget lets textures go at the rate new ones come in, whatever the size of each cut. The cut only decides how far under the line the cache sits (1,750 or 1,930 MB), a few percent of capacity. The lever on this machine is the budget's size, not the rule.

The patch stays in the comment above for anyone who wants to measure it on other hardware. I have added a note at the top of that comment pointing here.

> [@hclaria](https://github.com/hclaria) commented on GitHub · 2026-11-12 18:21 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2826#issuecomment-6832186804) **Correction to my numbers above: on the same route the effect is small, and I am not opening a follow-up for it** I ran the A/B the earlier comment lacked, and it does not hold up the play-session figures. **Setup.** Steam Deck OLED (Van Gogh `0x1435`, 16 GB), SteamOS 3.9.5, Mesa 26.2.0-devel (RADV). Game built from main at `c544828f`. Renderer: main at `284c9121` (it has this PR) plus the size-budget change, one binary for both arms, `RX_VK_EVICT_SIZE_EXCESS=0` against the default. `RX_VK_VRAM_MB=2000`, `RX_VK_LOW_SPEC=1`. Headless, a save in play, Link standing still and the camera turning the same way for the whole run, 240 s measured, runs alternated. | | 7/8 (run 1) | 7/8 (run 2) | excess + 64 MB (run 1) | excess + 64 MB (run 2) | |---|---|---|---|---| | fps | 23.23 | 23.32 | 22.99 | 23.19 | | draws a frame | 1,262 | 1,262 | 1,275 | 1,283 | | textures let go for the budget | 7,637 | 7,510 | 6,916 | 7,307 | | passes for the budget | 137 | 120 | 196 | 211 | | uploads a frame | 1.50 (0.89 MB) | 1.50 (0.86 MB) | 1.38 (0.78 MB) | 1.47 (0.83 MB) | | `a frame's uploads took` lines | 5, 366 ms | 5, 453 ms | 1, 104 ms | 2, 147 ms | | lowest window of 120 presents | 21.9 fps | 20.1 fps | 14.4 fps | 17.9 fps | | frame time, standard deviation | 1.52 ms | 2.04 ms | 4.94 ms | 3.03 ms | So: 6% fewer textures let go, 5% fewer uploads, fewer long upload frames, the same fps within the noise, and a wider spread of frame times with the change (more passes, each with its own work). **What was wrong with the earlier numbers.** The two play sessions were not the same route, and a system memory setting changed between them. A first scripted A/B with Link walking had the same fault: the two arms ended up looking at different views (1,320 against 1,961 draws), and two runs of the same arm differed by a third in textures let go (11,696 and 8,757). Only the turning camera gave runs that repeat. **Why the cut matters so little here.** Over a full turn this scene uses more textures than the 2,000 MB budget holds, so the budget lets textures go at the rate new ones come in, whatever the size of each cut. The cut only decides how far under the line the cache sits (1,750 or 1,930 MB), a few percent of capacity. The lever on this machine is the budget's size, not the rule. The patch stays in the comment above for anyone who wants to measure it on other hardware. I have added a note at the top of that comment pointing here. <!-- github-sync:c:6832186804 -->
Commenting is not possible because the repository is archived.
No description provided.