Skip to content

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

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
klikso:perf/evict-excess
Nov 12, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
klikso:perf/evict-excess

Conversation

@klikso

@klikso klikso commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

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 ba005ca with #2711, #2731 (at bce3d17, 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 perf(vk): an import mostly outside every game pool is trimmed back to its live parts #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 perf(vk): an import mostly outside every game pool is trimmed back to its live parts #2825's branch (460 passed; the 2 that fail there fail on main here too), not again on this one.

🤖 Generated with Claude Code

… an eighth of the texture cache

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 <noreply@anthropic.com>
@klikso

klikso commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

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.

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread gpu/vk/vram_compact.h
inline constexpr uint64_t kExcessSlack = 64ull << 20;
inline uint64_t rule_target(uint64_t total, uint64_t size_budget, uint64_t rule_budget) {
uint64_t to = rule_target(total, size_budget);
if (rule_budget < size_budget && total > rule_budget) to = std::min(to, rule_budget - std::min(rule_budget, kExcessSlack));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread gpu/vk/vk_texture.cpp
@@ -4303,7 +4304,11 @@ void texture_frame(uint64_t completed) {
budget = std::min(budget, total_bytes > over ? total_bytes - over : 0);
rule_budget = std::min(rule_budget, total_bytes > over ? total_bytes - over : 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hclaria

hclaria commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

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 c544828 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);

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 added a commit that referenced this pull request Nov 12, 2026
… its live parts (#2825)

**Why.** On an 8 GB card, Castle Town Market runs at ~54 fps with almost
no long frames when it is loaded from a save, and at ~45 fps with a long
frame every second or two when the player walks in from Hyrule Field,
for as long as they stay. Part of the difference is VRAM the previous
scene's pools still hold: imports never shrink.

- An import only grows: a binding across its end makes it again, larger,
and a pool the game binds whole is imported whole. A pool's VRAM mirror,
and its write-back shadow, are its import's whole range.
- When the game frees a scene's large pools and makes the next scene's
smaller ones at the same addresses, the import keeps the old range. It
is bound every frame through the small live pool inside it, so no idle
rule ever lets its mirror go.
- Measured (the two largest; from a local diagnostic that lists the live
mirrors, not in this PR): the field's 213 MB pool at `0x108c711000`
leaves a **224 MB** mirror where the Market loaded from a save has **48
MB**; the culling lists' pool is **152 MB** where it has **104 MB**.
With their shadows that is ~260 MB the scene does not bind, on a card
where the Market's textures sit right at the 90% line.

This is the launch-to-launch "fast or slow Market" from #995 and my note
on #2685: it was not the launch, it was whether the save was in the
Market or in the field.

## What

`gpu/vk/mirror_trim.h` (rules only) and `mirrors_trim()` in
`vk_memory.cpp`, called from `memory_begin_submission` every 64
submissions:

- **Which import.** One with a mirror whose game pools (the host's
table, each out to its 4 MB chunks, parts closer than 8 MB joined) leave
at least 24 MB and a quarter of it outside every pool, in 1 to 4 live
parts, and that has stayed so for 1800 submissions (~5 s). Only while
VRAM is over 80% of the allowance: with room to spare nothing is gained.
- **How.** Its live parts are imported first (a failed import leaves
everything as it was). Then it is retired with `retire_pool`, as a pool
made again larger is, and each part gets its page table entries and a
mirror **in the same submission's uploads**, before anything is
recorded. Each new mirror takes what the GPU wrote from the old one
through `mirror_make`'s existing inherit (pages the CPU stamped since
come from guest memory, as there). The old mirror goes once the GPU is
done with it.
- **Room.** Nothing is trimmed unless the new mirrors fit the mirror
budget beside the old one.
- **No thrash.** A range the game binds across again right after a trim
is made larger again by the existing path; it then waits 4x and 16x
longer, and after three trims in ~2 minutes it is left alone. The count
starts over after 36,000 submissions without a trim, so a scene change
every few minutes over a session is still trimmed.
- **Not touched:** an import with no game pool in it at all (nothing
binds it: the idle rules, and #2759's retire of finalized pools, cover
that case; this PR is the partly-live one, which #2759 leaves as
"alive"), parallel replay segments, cards without mirrors.
- `RX_VK_MIRROR_TRIM=0`: off. One log line a trim.

What is given up: what the GPU wrote into the **dead** part of the old
mirror and never wrote back. No game pool covers that memory any more,
so nothing can bind it; a pool the game makes there later gets a mirror
from guest memory, as any new pool does.

## 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` ba005ca with #2711, #2731 (at bce3d17, where it
was still on by default), #2685 and #2709 merged locally (the VRAM
picture on this card without #2711 is the #2690 regression). 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 fourth arm
adds #2826 (the 90% rule evicting its excess instead of an eighth of the
cache), which is the other half of the same stutter.

| 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** | 45.7 / 47.2 / 47.4 | 66 / 37 / 28 | 48.5 | 4.7 |
| #2826 alone | 43.3 / 46.6 / 46.2 | 75 / 25 / 60 | 47.0 | 7.0 |
| this PR + #2826 | 49.7 / 48.8 / 49.3 | 10 / 15 / 22 | 50.6 | 0.3 |

- The first 20 s are the arrival itself (the trim only starts ~6 s after
the old pools die), and are about the same in every arm.
- Both trims happen in every run: `pool 0x108c000000..0x109a400000 (228
MB) trimmed to its 1 live part(s), 48 MB` and
`0x1178400000..0x1181c00000 (152 MB) ... 104 MB`. Mirrors at the end of
a run: 1,584 MB against 1,820 MB. The new imports took under 2 ms each
(no `imported in` line, which is logged from 2 ms).
- Loaded from a save in the Market, no import qualifies and no trim runs
(0 in two 30 s runs, 47.1 / 47.9 fps; with this and #2826 off 46.5 /
46.0). Those are lower than the 53-55 fps the same load gave in the
morning: the driver's allowance was ~100 MB lower by then (6,944 against
7,043 MB), which is enough to put the direct load over the 90% line too.

**A session:** two rounds of 12 scenes by teleport (Hyrule Field, Kokiri
Forest, Market, Kakariko, Lost Woods, Goron City, Zora's Domain,
Graveyard, Death Mountain Trail, Zora's River, Sacred Forest Meadow,
Gerudo Valley), 20 s of camera turning in each, with the flash detector
on (`RX_VK_FLASH=1`, `RX_VK_SHOT_EVERY=1`): 24 of 24 scenes reached,
**13 trims (600 MB given back in all), no flash detected, no device
loss, no failed import**. The mirrors moved between 1.2 and 2.0 GB and
ended at 1,656 MB; the same tour before this sat at the 2,000 MB budget.
Screenshots after the camera turn in Kokiri Forest (foliage, grass),
Hyrule Field and the Market look right; I did not watch the runs live.

## Not tested

- AMD, Intel, Linux. On Windows the retired import goes back by the
existing rules; AMD and Intel hold retired imports for a grace time
there (`retired_give_back.h`), which this does not change but does feed.
- `RX_VK_PARALLEL` (the trim does not run inside its segments, and was
not run with it on).
- A card with more VRAM than it needs: the 80% gate means it should
never run there.
- Long play (hours).

## Tests

- New `tests/test_mirror_trim.py` compiles and runs
`tests/test_mirror_trim.cpp` (the Market's case, imports their pools
fill, a small dead share, no live pool, parts joined and kept apart, too
many parts, the waits and the count that starts over, the VRAM gate) and
checks `vk_memory.cpp` keeps the order: parts imported before the
retire, mirrors made before the old one stops waiting to be inherited,
room in the budget first.
- `nx2/test_import_windows.py`'s model of `Pool` gets the two new fields
`pool_lookup` carries over; `tests/test_texel_buffer_ownership.py`
counts the trim's rollback among the callers of `destroy_import_buffer`
(10).
- `python -I -B tests/run_all.py` on Windows: 460 passed, 2 failed, 34
skipped; the two (`nx2/test_nup_paths.py`,
`tests/test_input_background.py`) fail on unchanged `main` here too.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Co-authored-by: hckrman101 <239247847+hckrman101@users.noreply.github.com>
@hckrman101
hckrman101 merged commit ba3d3f7 into NotRexed:main Nov 12, 2026
15 checks passed
klikso added a commit to klikso/Hookshot that referenced this pull request Nov 12, 2026
… its 7/8

Review of NotRexed#2826 (hckrman101): rule_budget was the lower of the 90% rule's and the 97% rule's
budgets, so the driver's limit got the 64 MB slack too. Past 97% the driver pages VRAM out, which
costs more than a texture made again, so that rule keeps the eighth of headroom: texture_frame now
passes the 90% rule's budget to the excess-only target and the limit's to the two-argument form,
and takes the lower. RX_VK_EVICT_EXCESS is read by the switches' rule.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@klikso

klikso commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

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

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants