feat(vk): RX_VK_MEMORY_PRIORITY tells the driver which memory to keep in VRAM when it runs short (off by default) #560

Open
Jiro113 wants to merge 4 commits from Jiro113/Longshot:feat/vk-memory-priority into main
First-time contributor

Type: feat · Risk: low · Tested on: Windows 11, RTX 4090 (NVIDIA 621.24) · Needs: Windows with an NVIDIA card of 8 GB or less; Windows with an AMD or Intel card; Linux, any GPU

Why

When the graphics card runs short of memory, because other programs hold part of it or because the card is small for what the game has loaded, the system moves some of the game's memory out of the card and the frame rate collapses: in the test below, from 85 to 90 fps down to 27, then under 9, in the same scene. Today the game has no say in which of its memory goes. This PR lets it tell the driver what to keep (the pictures it draws into) and what may go first (the textures), behind a switch that is off by default, with the numbers from one PC.

See #586 (4 GB cards short of VRAM) and #130 (a whole session at the driver's VRAM limit on an 8 GB card, other programs holding part of it): both are the situation the switch is for. It has not run on such a card yet, so it closes neither. #441 (a 4 GB RTX 3050 laptop at 9 to 20 fps with a 4.8 GB budget) may be the same situation; its cause isn't known.

What changed

The renderer can now ask the device for VK_EXT_memory_priority and give its memory a priority: the render targets 1, the sampled textures' pool 0.25, the rest the 0.5 everything has today. It does so only with RX_VK_MEMORY_PRIORITY set; unset, the extension isn't asked for and nothing changes.

  • renderer, the rule: gpu/vk/memory_priority.h (new, no Vulkan in it; read it first). memory_priority::parse() reads RX_VK_MEMORY_PRIORITY: off by default; 1 gives the render targets 1, the sampled textures' pool 0.25 and the rest 0.5; <targets>,<textures> sets the first two for a test, each a number from 0 to 1 with a decimal point. Anything else with a comma in it (0,5 meant as a half, nan,0.25, three numbers) is not guessed at: it gives the usual 1 and 0.25, and the log says the value wasn't read.
  • renderer, the device: gpu/vk/vk_main.cpp asks for VK_EXT_memory_priority and its feature only with the switch on, and keeps the answer in dev.memory_priority (gpu/vk/vk_internal.h).
  • renderer, the allocator: gpu/vk/vk_vma.cpp. memory_tiers() holds the rule, the allocator is made with VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT when the device has the extension, and every pool (pi.priority) and every allocation (create_image, create_buffer, create_buffer_cached, create_device_buffer, alloc_sparse_block) sets a priority. All of them, because with that bit VMA gives a custom pool or a dedicated allocation that sets none the priority 0: that nothing is left at 0 is the point to check, and tests/test_memory_priority.py reads the source for it. One log line at the start says the three numbers, or that the device has no such extension with its feature.
  • what keeps the driver's 0.5, unchanged here: memory the renderer allocates without VMA (vkAllocateMemory: the sparse textures' 64 MB chunks in vk_texture.cpp, two small buffers the CPU reads, the imports of guest memory), and an image its pool has no room for when it lands in one of VMA's shared blocks. A priority for the sparse chunks, which are textures, is left for a follow-up: not measured, so not added.
  • checks: tests/test_memory_priority.py (new) runs the rule and reads vk_vma.cpp and vk_main.cpp for a pool or an allocation left without a priority; tests/test_blocks_at_start.py gains the pool's priority in its stub and two cases.
  • docs: the switch's row in docs/development.md, and twelve files in docs/tested/.

Measurements

Verdict: it helped where the game's memory was being moved out of VRAM, on the one PC it ran on: with about 0.5 GiB out a frame took 16.5 to 21.6 ms with the switch on against 30.6 and 33.6 ms with it unset, and with about 1.3 GiB out 24 to 29 ms against 109 ms and more. With nothing short it cost nothing measurable (7.2 to 7.9 ms either way). The numbers are from the build named in Baseline, not from this head, which has not run in the game.

Baseline: both sides are one build, main @ 5e072fc8 (2026-11-14) with this PR's four renderer files on it (memory_priority.h, vk_internal.h, vk_main.cpp, vk_vma.cpp) and DLSS, with RX_VK_MEMORY_PRIORITY unset against set. main against this PR wasn't the comparison because the switch is off by default: with it unset the renderer doesn't ask for the extension and VMA ignores the priorities, which is main's behaviour. Comparing the switch off and on in one build keeps everything else the same.
Since the runs: the PR is now on main @ 82886e89, 29 commits after 5e072fc8, and was not measured again. What main changed in the renderer since: a render thread for the replay (on by default from 12 logical processors, so on at this PC), a renderer built with a play profile, and the texture budget and its squeeze on integrated GPUs; in this PR's own files, vk_vma.cpp now starts the textures' pool with no block on an integrated GPU (a dedicated card's count is as it was) and vk_main.cpp gained an Intel Arc case. None of it adds a pool or an allocation, or sets a priority (read off main's diff of gpu/vk), and at the head tests/test_memory_priority.py still finds none left without one: on a dedicated card the switch sets the same priorities on the same pools and allocations as in the runs. What that does to the frame time at the head was not seen: the frame times are the measured build's and would not be the same with the render thread, and the gain, though it is the switch off against on inside one build either way, is to be confirmed there (Must be tested, first item). Since the runs this PR's own memory_priority.h has gained a comment and the pair's strict reading, which reads the three values that were run (1, 0.5,0.25, 1,0.5) as before, and vk_vma.cpp two log lines.
Hardware: RTX 4090 24 GB, NVIDIA 621.24, Core i9-13900K, 64 GB RAM, Windows 11 Home 10.0.26300
Scene: Hyrule Field as a child, standing still; output 2560x1440 with ultrawide on (presented 5120x1440, SDR), DLSS on (2560x720 to 5120x1440, preset K), frame generation off, V-Sync Auto (mailbox), quality preset 6, texture budget 6144 MB, uncapped (HOS_FPS_MAX=1000), RX_VK_AUTO_TUNE=0
Method: fourteen launches, twelve usable runs, in the order off, on, on, (off, void: the window's focus changed and the game stopped answering; 1,0.5, void: started beside that stuck game and stopped by hand), off, 1,0.5, 0.5,0.25; then, with RX_VK_IMAGE_PREALLOC=9, in the order 1, unset, unset, 1, 0.5,0.25, 1 (the 5-minute hold). Each run starts the game, gets into play, teleports to the scene and waits 20 s there before its first window (the warm-up). In each run a 30 s window with nothing else on the card, then VRAM occupants in other processes (12288 MB, then 2048 MB ones), each followed by 20 s of settling and a 30 s window, until 800 MB or more of the game's device memory was out of VRAM (the driver's count of the game's use of the device-local heap minus Windows' Dedicated Usage for the process), then 3 more windows (10 in the last run), then one with the occupants gone. A number is the median of a window's vk: GPU time ... ms a frame log lines, with the mean of its fps lines in brackets. A window is used only in play, on the same map, with the world drawn, no focus change and every occupant alive.
How to reproduce: teleport to Hyrule Field as a child with the settings above and stand still; start processes that each allocate and touch VRAM (12288 MB, then 2048 MB at a time) until the game's Dedicated Usage in Windows falls 800 MB below the vk: VRAM: ... MB used line; read the vk: GPU time and fps lines; repeat with RX_VK_MEMORY_PRIORITY=1. RX_VK_IMAGE_PREALLOC=9 gives the textures' pool the 9 blocks of an 8 GiB card.

With RX_VK_IMAGE_PREALLOC=9 on this 24 GB card (the textures' pool settles at 9 blocks, 1152 MB, holding 791 to 926 MB of images). GPU time a frame, one value a run:

Occupants (the game's memory out of VRAM) Before, RX_VK_MEMORY_PRIORITY unset: main with this PR's renderer files @ 5e072fc8 After, RX_VK_MEMORY_PRIORITY=1: main with this PR's renderer files @ 5e072fc8 After, RX_VK_MEMORY_PRIORITY=0.5,0.25: main with this PR's renderer files @ 5e072fc8
No occupant 7.75 ms (86.8 fps), 7.2 ms (90.9) 7.5 ms (89.6), 7.8 ms (85.0), 7.9 ms (85.2) 7.8 ms (85.4)
24576 MB (nothing out) 8.1 ms (82.5), 7.9 ms (82.9) 8.1 ms (83.4), 8.4 ms (77.2), 8.2 ms (80.6) 8.2 ms (80.4)
26624 MB (495 to 622 MB out) 30.6 ms (26.8), 33.6 ms (27.0) 16.5 ms (48.9), 21.6 ms (39.8), 20.05 ms (42.1) 17.95 ms (45.9)
28672 MB (1261 to 1357 MB out) 109.25 ms (8.9; 2 samples), 178.5 ms (5.5; 1 sample, the amount out not readable) 24.2 ms (36.2), 28.5 ms (31.2), 29.05 ms (30.9) 28.25 ms (31.9)
Held there, 3 more windows 110.5 to 114.25 ms (8.5 to 8.8; 2 or 3 samples a window) in one run; the other run went on, below 24.1 to 24.7 ms; 22.85 to 27.45 ms; 10 windows over 5 minutes: 26.2 to 35.4 ms (25.3 to 33.9 fps) 27.75 to 28.65 ms
Occupants gone 7.2 ms (89.2), 7.8 ms (85.1) 7.8 ms (84.7), 7.8 ms (84.4), 7.6 ms (85.6) 7.8 ms (83.5)

With the textures' pool as this card makes it (24 blocks, 3072 MB, which it keeps under pressure while holding about 800 to 930 MB of images):

Occupants (the game's memory out of VRAM) Before, RX_VK_MEMORY_PRIORITY unset: main with this PR's renderer files @ 5e072fc8 After, RX_VK_MEMORY_PRIORITY=1: main with this PR's renderer files @ 5e072fc8 After, RX_VK_MEMORY_PRIORITY=0.5,0.25: main with this PR's renderer files @ 5e072fc8 After, RX_VK_MEMORY_PRIORITY=1,0.5: main with this PR's renderer files @ 5e072fc8
No occupant 7.4 ms (89.6 fps), 7.5 ms (85.9) 7.3 ms (90.7), 7.6 ms (88.0) 7.5 ms (89.4) 7.6 ms (88.6)
22528 MB (nothing out, the allowance near what the game uses) 14.2 ms (55.6), 7.5 ms (87.4) 13.75 ms (57.5), 14.2 ms (56.3) 12.45 ms (65.7) 14.15 ms (56.2)
24576 MB 30.6 ms (29.0) with 1325 MB out, 18.6 ms (44.7) with 750 MB out 16.4 ms (48.5) with 974 MB out, 17.85 ms (46.9) with 1134 MB out 16.1 ms (50.8) with 910 MB out 16.6 ms (49.1) with 1261 MB out
Held there, 3 more windows 30.4, 30.5, 30.7 ms in the first run; the second went on, below 15.9, 16.0, 16.7 ms; 16.5, 16.6, 16.2 ms 15.8, 15.8, 15.8 ms 17.7, 24.2, 67.55 ms (14.8 fps in the last)

Read with these limits:

  • The 9-block pairs are the result: two runs each way at the same step, and a factor of 1.4 to 2 at about 0.5 GiB out, 3.8 and more at about 1.3 GiB (where the unset windows have 2 samples and 1). With the 24 blocks the gap is thin at the same step (18.6 ms unset with 750 MB out against 17.85 ms on with 1134 MB out).
  • Below about 6 fps the log has no memory line inside a 30 s window, so the 800 MB mark was not seen and two unset runs took more occupants than the others. Those states are apart from the tables: with 24 blocks, 170.85 ms (5.7 fps, 2 samples) at 26624 MB and 202.7 ms (4.8 fps, 2 samples) at 28672 MB with 3130 MB out; with 9 blocks, 210.5 ms (4.7 fps, 1 sample) at 30720 MB and 228.6 ms (4.3 fps, 1 sample) at 32768 MB with 1980 MB out. In the tables a window with 3 samples or fewer says so.
  • It costs nothing measurable when nothing is short: the rows with no occupant and with the occupants gone are within the runs' spread (7.2 to 7.9 ms either way). "Nothing measurable" means within about 0.5 ms on two or three runs each way; a smaller cost would not show here.
  • The textures' lower priority looks like the lever, which one run each suggests and does not prove: 0.5,0.25 did as well as 1 (one run with each pool size), and the render targets raised alone (1,0.5, one run) started as well, lost it within 90 s and ended at 67.55 ms, slower than the switch unset at that step (30.4 to 30.7 ms). 1 keeps the render targets at 1 all the same: the measured build has it, it did no harm in five runs, and changing it would need the runs again.
  • One PC, one driver, one scene standing still, and VRAM taken by other processes on a 24 GB card, which is not the same as a card with 8 GB of its own. Hence off by default.
  • Seen on the way, not changed here: under pressure the textures' pool keeps its 24 start blocks (3072 MB) for about 800 MB of images, and with 9 blocks the memory started to leave VRAM one occupant later; and the renderer alone goes from about 7.5 to about 14 ms a frame when the allowance is near what it uses, with nothing out of VRAM yet.
-- RX_VK_MEMORY_PRIORITY unset (run p9off1), RX_VK_IMAGE_PREALLOC=9, 26624 MB of occupants, about 0.5 GiB of the game's memory out of VRAM
[hos Presentation Thread] vk: 40440 presents, 29.0 fps; a frame: 1761 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (14203 ms), 0 programs (0 failed), 833 textures (1847 MB)
[hos Presentation Thread] vk: GPU time 30.3 ms a frame (the longest submission 12.3 ms); 0.0 textures made a frame
[hos Presentation Thread] vk: 40560 presents, 28.9 fps; a frame: 1780 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (14203 ms), 0 programs (0 failed), 832 textures (1847 MB)
[hos Presentation Thread] vk: GPU time 30.3 ms a frame (the longest submission 12.3 ms); 0.0 textures made a frame
[hos Presentation Thread] vk: 40680 presents, 28.7 fps; a frame: 1776 draws (0 skipped), 159 dispatches, 161 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (14203 ms), 0 programs (0 failed), 832 textures (1847 MB)
[hos Presentation Thread] vk: GPU time 30.5 ms a frame (the longest submission 13.0 ms); 0.0 textures made a frame
[hos Presentation Thread] vk: VRAM: 5139 MB used of 4626 MB allowed (860 MB of it free in the blocks); textures 1847 MB (budget 262 MB), 832 textures
-- RX_VK_MEMORY_PRIORITY=1 (run p9on1), the same step
[hos host] vk: memory priority (RX_VK_MEMORY_PRIORITY, VK_EXT_memory_priority): render targets 1.00, textures 0.25, the rest 0.50
[hos Presentation Thread] vk: 42240 presents, 46.8 fps; a frame: 1702 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.5 uploads (0.2 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (13327 ms), 0 programs (0 failed), 810 textures (1822 MB)
[hos Presentation Thread] vk: GPU time 16.7 ms a frame (the longest submission 16.4 ms); 0.5 textures made a frame
[hos Presentation Thread] vk: 42360 presents, 49.7 fps; a frame: 1701 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (13327 ms), 0 programs (0 failed), 750 textures (1740 MB)
[hos Presentation Thread] vk: GPU time 16.5 ms a frame (the longest submission 9.8 ms); 0.0 textures made a frame
[hos Presentation Thread] vk: 42480 presents, 49.4 fps; a frame: 1702 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (13327 ms), 0 programs (0 failed), 750 textures (1740 MB)
[hos Presentation Thread] vk: GPU time 16.6 ms a frame (the longest submission 14.9 ms); 0.0 textures made a frame
[hos Presentation Thread] vk: VRAM: 5201 MB used of 4704 MB allowed (989 MB of it free in the blocks); textures 1740 MB (budget 272 MB), 750 textures

Flags and settings

One new switch, RX_VK_MEMORY_PRIORITY, in two forms; no menu row, ini key or build flag.

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
RX_VK_MEMORY_PRIORITY=1 Asks the device for VK_EXT_memory_priority and gives the renderer's memory a priority: render targets 1, sampled textures' pool 0.25, the rest 0.5 - (no such switch) Unset: off When the system moves the game's memory out of VRAM, the textures go before the render targets, and a frame takes about half the time or less Helps: NVIDIA on Windows when the game's memory is being moved out of VRAM (an RTX 4090 with other processes holding the VRAM). Doesn't help: a card with VRAM to spare, where nothing moves (the windows with no occupant are the same with and without it). Untested: AMD, Intel, Linux, any card with little VRAM of its own, a scene that moves Off for now: not measured enough (one PC, and no card with 8 GB or less has run it). On for everyone once such a card shows the same gain
RX_VK_MEMORY_PRIORITY=<targets>,<textures> The same with those two priorities, each from 0 to 1 - (no such switch) Unset: off As above, with the two numbers given Helps: 0.5,0.25 did as well as 1, in one run with each pool size. Doesn't help: 1,0.5 (the render targets raised alone) lost its gain within 90 s in its one run and ended at 67.55 ms a frame, slower than the 30.4 to 30.7 ms of the switch unset at that step. Untested: the same as above Off: only for testing

Testing

Tested: python -I -B tests/run_all.py --jobs 12 on Windows 11 at 2e2e6490: 521 passed, 14 failed, 43 skipped. The fourteen fail the same way on main @ 82886e89 on this PC (run on an exported copy of it, each failure's text read on both sides): tests/test_appimage_player_build.py, tests/test_bake_portability.py, tests/test_bug_report.py, tests/test_cpm_discovery.py, tests/test_env_on.py, tests/test_lift_cache.py, tests/test_lift_cleanup_error.py, tests/test_linux_preflight.py, tests/test_make_game_cache.py, tests/test_oot_state_tap.py, tests/test_readback_late.py, tests/test_tested_configurations.py, tests/test_vram_budget_row.py and tests/test_walk_key.py. (With the default 32 checks at once, one or two small fixtures also hit their 10 s limit, a different one each run; each passes alone.) tests/test_memory_priority.py and tests/test_blocks_at_start.py pass. The renderer alone (RX_VK_ONLY=1, no game) built from 2e2e6490 on both systems, 0 files failed: on Windows 11 with llvm-mingw 20261104 (longshot_vk.dll, built without the DLSS SDK), and on Ubuntu 22.04 (under WSL 2) the way CI's "longshot_vk.so (build check)" step builds it (RX_MARCH=x86-64-v3, RX_RELEASE=1, the shipped profile; longshot_vk.so); there tests/test_memory_priority.py, tests/test_blocks_at_start.py and tests/test_utf8_paths.py pass too. In the game, on the build named in Baseline: the twelve runs above on the RTX 4090, Hyrule Field, DLSS on; every run's log has DLSS started and no device-lost line, and the frame time came back to 7.2 to 7.8 ms once the occupants were gone. With the switch on, the log says the three priorities.

Not tested: this PR's head in the game (the runs' renderer is this PR's four files compiled on main @ 5e072fc8's tree; main's renderer has changed since, see Measurements, and the head's renderer was built but not run); a card with little VRAM of its own; AMD and Intel drivers; Linux in the game (built there, not run: no Linux with a GPU here); a device without the extension (the code path logs it and changes nothing; no such device here); the log line for a pair that isn't read (checked by tests/test_memory_priority.py: the rule, and that vk_vma.cpp logs it; not seen in the game); a scene that moves or loads; frame generation on; more than 5 minutes under pressure.

Must be tested:

  • The PC of the runs, at this PR's head: Hyrule Field with RX_VK_IMAGE_PREALLOC=9 and VRAM occupants until about 0.5 GiB of the game's memory is out of VRAM, RX_VK_MEMORY_PRIORITY unset then =1, expecting the vk: memory priority line at the start and a lower vk: GPU time with it on, as in the tables (by hand, the author: two interleaved runs each way, the vk: GPU time and fps lines)
  • Windows, an NVIDIA card with 8 GB or less: a heavy scene at a resolution that fills the VRAM, RX_VK_MEMORY_PRIORITY unset then =1, expecting the same or a better frame time with it on and no new stutter (by hand, test request T-MCA2: the vk: GPU time, fps and vk: VRAM lines of the log)
  • Windows, an AMD or Intel card: start the game with RX_VK_MEMORY_PRIORITY=1 and play a few minutes, expecting the vk: memory priority line at the start and the frame time of the switch unset (by hand, test request T-ATFL: the log's vk: memory priority and vk: GPU time lines)
  • Linux, any GPU maker: start the game with RX_VK_MEMORY_PRIORITY=1 and play a few minutes, expecting the vk: memory priority line at the start and the frame time of the switch unset (by hand, test request T-FKWJ: the same lines)

The three requests wait for a maintainer's testing wanted label before a tester can take them. The last three decide the default more than the merge: with the switch unset, which is how it ships, nothing of this runs.

Risks

What could break: nothing with the switch unset, which is how it ships: the extension isn't asked for. With it on, a driver may ignore the priorities, or use them in a way that suits another scene less (textures brought back late could stutter where the camera turns fast; not seen standing still). The test form can do worse than nothing: 1,0.5 ended slower than the switch unset in its one run.

Turning it off: unset RX_VK_MEMORY_PRIORITY; reverting the commit removes it.

Compatibility: Nothing changes: the switch is unset by default, and no save, longshot_options.ini key, shader or pipeline cache is touched. MkwVkHost and MkwVkRenderer are as they were, so MKWVK_API_VERSION stays.

Issues

See #586
See #130
See #441

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named).
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (Unset and 1: 2 and 3 runs with 9 blocks, 2 and 2 with 24; the two test pairs have one run a pool size and are said so.)
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md).
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (The renderer built from 2e2e6490 on both, see Testing; the change handles no path.)
  • Docs updated.

🤖 Generated with Claude Code

**Type:** feat · **Risk:** low · **Tested on:** Windows 11, RTX 4090 (NVIDIA 621.24) · **Needs:** Windows with an NVIDIA card of 8 GB or less; Windows with an AMD or Intel card; Linux, any GPU ## Why When the graphics card runs short of memory, because other programs hold part of it or because the card is small for what the game has loaded, the system moves some of the game's memory out of the card and the frame rate collapses: in the test below, from 85 to 90 fps down to 27, then under 9, in the same scene. Today the game has no say in which of its memory goes. This PR lets it tell the driver what to keep (the pictures it draws into) and what may go first (the textures), behind a switch that is off by default, with the numbers from one PC. See #586 (4 GB cards short of VRAM) and #130 (a whole session at the driver's VRAM limit on an 8 GB card, other programs holding part of it): both are the situation the switch is for. It has not run on such a card yet, so it closes neither. #441 (a 4 GB RTX 3050 laptop at 9 to 20 fps with a 4.8 GB budget) may be the same situation; its cause isn't known. ## What changed The renderer can now ask the device for `VK_EXT_memory_priority` and give its memory a priority: the render targets 1, the sampled textures' pool 0.25, the rest the 0.5 everything has today. It does so only with `RX_VK_MEMORY_PRIORITY` set; unset, the extension isn't asked for and nothing changes. - renderer, the rule: `gpu/vk/memory_priority.h` (new, no Vulkan in it; read it first). `memory_priority::parse()` reads `RX_VK_MEMORY_PRIORITY`: off by default; `1` gives the render targets 1, the sampled textures' pool 0.25 and the rest 0.5; `<targets>,<textures>` sets the first two for a test, each a number from 0 to 1 with a decimal point. Anything else with a comma in it (`0,5` meant as a half, `nan,0.25`, three numbers) is not guessed at: it gives the usual 1 and 0.25, and the log says the value wasn't read. - renderer, the device: `gpu/vk/vk_main.cpp` asks for `VK_EXT_memory_priority` and its feature only with the switch on, and keeps the answer in `dev.memory_priority` (`gpu/vk/vk_internal.h`). - renderer, the allocator: `gpu/vk/vk_vma.cpp`. `memory_tiers()` holds the rule, the allocator is made with `VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT` when the device has the extension, and every pool (`pi.priority`) and every allocation (`create_image`, `create_buffer`, `create_buffer_cached`, `create_device_buffer`, `alloc_sparse_block`) sets a priority. All of them, because with that bit VMA gives a custom pool or a dedicated allocation that sets none the priority 0: that nothing is left at 0 is the point to check, and `tests/test_memory_priority.py` reads the source for it. One log line at the start says the three numbers, or that the device has no such extension with its feature. - what keeps the driver's 0.5, unchanged here: memory the renderer allocates without VMA (`vkAllocateMemory`: the sparse textures' 64 MB chunks in `vk_texture.cpp`, two small buffers the CPU reads, the imports of guest memory), and an image its pool has no room for when it lands in one of VMA's shared blocks. A priority for the sparse chunks, which are textures, is left for a follow-up: not measured, so not added. - checks: `tests/test_memory_priority.py` (new) runs the rule and reads `vk_vma.cpp` and `vk_main.cpp` for a pool or an allocation left without a priority; `tests/test_blocks_at_start.py` gains the pool's priority in its stub and two cases. - docs: the switch's row in `docs/development.md`, and twelve files in `docs/tested/`. ## Measurements **Verdict:** it helped where the game's memory was being moved out of VRAM, on the one PC it ran on: with about 0.5 GiB out a frame took 16.5 to 21.6 ms with the switch on against 30.6 and 33.6 ms with it unset, and with about 1.3 GiB out 24 to 29 ms against 109 ms and more. With nothing short it cost nothing measurable (7.2 to 7.9 ms either way). The numbers are from the build named in Baseline, not from this head, which has not run in the game. Baseline: both sides are one build, main @ 5e072fc8 (2026-11-14) with this PR's four renderer files on it (`memory_priority.h`, `vk_internal.h`, `vk_main.cpp`, `vk_vma.cpp`) and DLSS, with `RX_VK_MEMORY_PRIORITY` unset against set. main against this PR wasn't the comparison because the switch is off by default: with it unset the renderer doesn't ask for the extension and VMA ignores the priorities, which is main's behaviour. Comparing the switch off and on in one build keeps everything else the same. Since the runs: the PR is now on main @ 82886e89, 29 commits after 5e072fc8, and was not measured again. What main changed in the renderer since: a render thread for the replay (on by default from 12 logical processors, so on at this PC), a renderer built with a play profile, and the texture budget and its squeeze on integrated GPUs; in this PR's own files, `vk_vma.cpp` now starts the textures' pool with no block on an integrated GPU (a dedicated card's count is as it was) and `vk_main.cpp` gained an Intel Arc case. None of it adds a pool or an allocation, or sets a priority (read off main's diff of `gpu/vk`), and at the head `tests/test_memory_priority.py` still finds none left without one: on a dedicated card the switch sets the same priorities on the same pools and allocations as in the runs. What that does to the frame time at the head was not seen: the frame times are the measured build's and would not be the same with the render thread, and the gain, though it is the switch off against on inside one build either way, is to be confirmed there (Must be tested, first item). Since the runs this PR's own `memory_priority.h` has gained a comment and the pair's strict reading, which reads the three values that were run (`1`, `0.5,0.25`, `1,0.5`) as before, and `vk_vma.cpp` two log lines. Hardware: RTX 4090 24 GB, NVIDIA 621.24, Core i9-13900K, 64 GB RAM, Windows 11 Home 10.0.26300 Scene: Hyrule Field as a child, standing still; output 2560x1440 with ultrawide on (presented 5120x1440, SDR), DLSS on (2560x720 to 5120x1440, preset K), frame generation off, V-Sync Auto (mailbox), quality preset 6, texture budget 6144 MB, uncapped (`HOS_FPS_MAX=1000`), `RX_VK_AUTO_TUNE=0` Method: fourteen launches, twelve usable runs, in the order off, on, on, (off, void: the window's focus changed and the game stopped answering; `1,0.5`, void: started beside that stuck game and stopped by hand), off, `1,0.5`, `0.5,0.25`; then, with `RX_VK_IMAGE_PREALLOC=9`, in the order `1`, unset, unset, `1`, `0.5,0.25`, `1` (the 5-minute hold). Each run starts the game, gets into play, teleports to the scene and waits 20 s there before its first window (the warm-up). In each run a 30 s window with nothing else on the card, then VRAM occupants in other processes (12288 MB, then 2048 MB ones), each followed by 20 s of settling and a 30 s window, until 800 MB or more of the game's device memory was out of VRAM (the driver's count of the game's use of the device-local heap minus Windows' Dedicated Usage for the process), then 3 more windows (10 in the last run), then one with the occupants gone. A number is the median of a window's `vk: GPU time ... ms a frame` log lines, with the mean of its `fps` lines in brackets. A window is used only in play, on the same map, with the world drawn, no focus change and every occupant alive. How to reproduce: teleport to Hyrule Field as a child with the settings above and stand still; start processes that each allocate and touch VRAM (12288 MB, then 2048 MB at a time) until the game's Dedicated Usage in Windows falls 800 MB below the `vk: VRAM: ... MB used` line; read the `vk: GPU time` and `fps` lines; repeat with `RX_VK_MEMORY_PRIORITY=1`. `RX_VK_IMAGE_PREALLOC=9` gives the textures' pool the 9 blocks of an 8 GiB card. With `RX_VK_IMAGE_PREALLOC=9` on this 24 GB card (the textures' pool settles at 9 blocks, 1152 MB, holding 791 to 926 MB of images). GPU time a frame, one value a run: | Occupants (the game's memory out of VRAM) | Before, RX_VK_MEMORY_PRIORITY unset: main with this PR's renderer files @ 5e072fc8 | After, RX_VK_MEMORY_PRIORITY=1: main with this PR's renderer files @ 5e072fc8 | After, RX_VK_MEMORY_PRIORITY=0.5,0.25: main with this PR's renderer files @ 5e072fc8 | |---|---|---|---| | No occupant | 7.75 ms (86.8 fps), 7.2 ms (90.9) | 7.5 ms (89.6), 7.8 ms (85.0), 7.9 ms (85.2) | 7.8 ms (85.4) | | 24576 MB (nothing out) | 8.1 ms (82.5), 7.9 ms (82.9) | 8.1 ms (83.4), 8.4 ms (77.2), 8.2 ms (80.6) | 8.2 ms (80.4) | | 26624 MB (495 to 622 MB out) | 30.6 ms (26.8), 33.6 ms (27.0) | 16.5 ms (48.9), 21.6 ms (39.8), 20.05 ms (42.1) | 17.95 ms (45.9) | | 28672 MB (1261 to 1357 MB out) | 109.25 ms (8.9; 2 samples), 178.5 ms (5.5; 1 sample, the amount out not readable) | 24.2 ms (36.2), 28.5 ms (31.2), 29.05 ms (30.9) | 28.25 ms (31.9) | | Held there, 3 more windows | 110.5 to 114.25 ms (8.5 to 8.8; 2 or 3 samples a window) in one run; the other run went on, below | 24.1 to 24.7 ms; 22.85 to 27.45 ms; 10 windows over 5 minutes: 26.2 to 35.4 ms (25.3 to 33.9 fps) | 27.75 to 28.65 ms | | Occupants gone | 7.2 ms (89.2), 7.8 ms (85.1) | 7.8 ms (84.7), 7.8 ms (84.4), 7.6 ms (85.6) | 7.8 ms (83.5) | With the textures' pool as this card makes it (24 blocks, 3072 MB, which it keeps under pressure while holding about 800 to 930 MB of images): | Occupants (the game's memory out of VRAM) | Before, RX_VK_MEMORY_PRIORITY unset: main with this PR's renderer files @ 5e072fc8 | After, RX_VK_MEMORY_PRIORITY=1: main with this PR's renderer files @ 5e072fc8 | After, RX_VK_MEMORY_PRIORITY=0.5,0.25: main with this PR's renderer files @ 5e072fc8 | After, RX_VK_MEMORY_PRIORITY=1,0.5: main with this PR's renderer files @ 5e072fc8 | |---|---|---|---|---| | No occupant | 7.4 ms (89.6 fps), 7.5 ms (85.9) | 7.3 ms (90.7), 7.6 ms (88.0) | 7.5 ms (89.4) | 7.6 ms (88.6) | | 22528 MB (nothing out, the allowance near what the game uses) | 14.2 ms (55.6), 7.5 ms (87.4) | 13.75 ms (57.5), 14.2 ms (56.3) | 12.45 ms (65.7) | 14.15 ms (56.2) | | 24576 MB | 30.6 ms (29.0) with 1325 MB out, 18.6 ms (44.7) with 750 MB out | 16.4 ms (48.5) with 974 MB out, 17.85 ms (46.9) with 1134 MB out | 16.1 ms (50.8) with 910 MB out | 16.6 ms (49.1) with 1261 MB out | | Held there, 3 more windows | 30.4, 30.5, 30.7 ms in the first run; the second went on, below | 15.9, 16.0, 16.7 ms; 16.5, 16.6, 16.2 ms | 15.8, 15.8, 15.8 ms | 17.7, 24.2, 67.55 ms (14.8 fps in the last) | Read with these limits: - The 9-block pairs are the result: two runs each way at the same step, and a factor of 1.4 to 2 at about 0.5 GiB out, 3.8 and more at about 1.3 GiB (where the unset windows have 2 samples and 1). With the 24 blocks the gap is thin at the same step (18.6 ms unset with 750 MB out against 17.85 ms on with 1134 MB out). - Below about 6 fps the log has no memory line inside a 30 s window, so the 800 MB mark was not seen and two unset runs took more occupants than the others. Those states are apart from the tables: with 24 blocks, 170.85 ms (5.7 fps, 2 samples) at 26624 MB and 202.7 ms (4.8 fps, 2 samples) at 28672 MB with 3130 MB out; with 9 blocks, 210.5 ms (4.7 fps, 1 sample) at 30720 MB and 228.6 ms (4.3 fps, 1 sample) at 32768 MB with 1980 MB out. In the tables a window with 3 samples or fewer says so. - It costs nothing measurable when nothing is short: the rows with no occupant and with the occupants gone are within the runs' spread (7.2 to 7.9 ms either way). "Nothing measurable" means within about 0.5 ms on two or three runs each way; a smaller cost would not show here. - The textures' lower priority looks like the lever, which one run each suggests and does not prove: `0.5,0.25` did as well as `1` (one run with each pool size), and the render targets raised alone (`1,0.5`, one run) started as well, lost it within 90 s and ended at 67.55 ms, slower than the switch unset at that step (30.4 to 30.7 ms). `1` keeps the render targets at 1 all the same: the measured build has it, it did no harm in five runs, and changing it would need the runs again. - One PC, one driver, one scene standing still, and VRAM taken by other processes on a 24 GB card, which is not the same as a card with 8 GB of its own. Hence off by default. - Seen on the way, not changed here: under pressure the textures' pool keeps its 24 start blocks (3072 MB) for about 800 MB of images, and with 9 blocks the memory started to leave VRAM one occupant later; and the renderer alone goes from about 7.5 to about 14 ms a frame when the allowance is near what it uses, with nothing out of VRAM yet. ``` -- RX_VK_MEMORY_PRIORITY unset (run p9off1), RX_VK_IMAGE_PREALLOC=9, 26624 MB of occupants, about 0.5 GiB of the game's memory out of VRAM [hos Presentation Thread] vk: 40440 presents, 29.0 fps; a frame: 1761 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (14203 ms), 0 programs (0 failed), 833 textures (1847 MB) [hos Presentation Thread] vk: GPU time 30.3 ms a frame (the longest submission 12.3 ms); 0.0 textures made a frame [hos Presentation Thread] vk: 40560 presents, 28.9 fps; a frame: 1780 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (14203 ms), 0 programs (0 failed), 832 textures (1847 MB) [hos Presentation Thread] vk: GPU time 30.3 ms a frame (the longest submission 12.3 ms); 0.0 textures made a frame [hos Presentation Thread] vk: 40680 presents, 28.7 fps; a frame: 1776 draws (0 skipped), 159 dispatches, 161 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (14203 ms), 0 programs (0 failed), 832 textures (1847 MB) [hos Presentation Thread] vk: GPU time 30.5 ms a frame (the longest submission 13.0 ms); 0.0 textures made a frame [hos Presentation Thread] vk: VRAM: 5139 MB used of 4626 MB allowed (860 MB of it free in the blocks); textures 1847 MB (budget 262 MB), 832 textures -- RX_VK_MEMORY_PRIORITY=1 (run p9on1), the same step [hos host] vk: memory priority (RX_VK_MEMORY_PRIORITY, VK_EXT_memory_priority): render targets 1.00, textures 0.25, the rest 0.50 [hos Presentation Thread] vk: 42240 presents, 46.8 fps; a frame: 1702 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.5 uploads (0.2 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (13327 ms), 0 programs (0 failed), 810 textures (1822 MB) [hos Presentation Thread] vk: GPU time 16.7 ms a frame (the longest submission 16.4 ms); 0.5 textures made a frame [hos Presentation Thread] vk: 42360 presents, 49.7 fps; a frame: 1701 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (13327 ms), 0 programs (0 failed), 750 textures (1740 MB) [hos Presentation Thread] vk: GPU time 16.5 ms a frame (the longest submission 9.8 ms); 0.0 textures made a frame [hos Presentation Thread] vk: 42480 presents, 49.4 fps; a frame: 1702 draws (0 skipped), 159 dispatches, 162 passes, 137 barriers, 6 submits, 0.0 uploads (0.0 MB), 80 ub updates (56 overlays), GPU-bound wait 0.0 ms; 7206 pipelines (13327 ms), 0 programs (0 failed), 750 textures (1740 MB) [hos Presentation Thread] vk: GPU time 16.6 ms a frame (the longest submission 14.9 ms); 0.0 textures made a frame [hos Presentation Thread] vk: VRAM: 5201 MB used of 4704 MB allowed (989 MB of it free in the blocks); textures 1740 MB (budget 272 MB), 750 textures ``` ### Flags and settings One new switch, `RX_VK_MEMORY_PRIORITY`, in two forms; no menu row, ini key or build flag. | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `RX_VK_MEMORY_PRIORITY=1` | Asks the device for `VK_EXT_memory_priority` and gives the renderer's memory a priority: render targets 1, sampled textures' pool 0.25, the rest 0.5 | - (no such switch) | Unset: off | When the system moves the game's memory out of VRAM, the textures go before the render targets, and a frame takes about half the time or less | Helps: NVIDIA on Windows when the game's memory is being moved out of VRAM (an RTX 4090 with other processes holding the VRAM). Doesn't help: a card with VRAM to spare, where nothing moves (the windows with no occupant are the same with and without it). Untested: AMD, Intel, Linux, any card with little VRAM of its own, a scene that moves | Off for now: not measured enough (one PC, and no card with 8 GB or less has run it). On for everyone once such a card shows the same gain | | `RX_VK_MEMORY_PRIORITY=<targets>,<textures>` | The same with those two priorities, each from 0 to 1 | - (no such switch) | Unset: off | As above, with the two numbers given | Helps: `0.5,0.25` did as well as `1`, in one run with each pool size. Doesn't help: `1,0.5` (the render targets raised alone) lost its gain within 90 s in its one run and ended at 67.55 ms a frame, slower than the 30.4 to 30.7 ms of the switch unset at that step. Untested: the same as above | Off: only for testing | ## Testing **Tested:** `python -I -B tests/run_all.py --jobs 12` on Windows 11 at 2e2e6490: 521 passed, 14 failed, 43 skipped. The fourteen fail the same way on main @ 82886e89 on this PC (run on an exported copy of it, each failure's text read on both sides): `tests/test_appimage_player_build.py`, `tests/test_bake_portability.py`, `tests/test_bug_report.py`, `tests/test_cpm_discovery.py`, `tests/test_env_on.py`, `tests/test_lift_cache.py`, `tests/test_lift_cleanup_error.py`, `tests/test_linux_preflight.py`, `tests/test_make_game_cache.py`, `tests/test_oot_state_tap.py`, `tests/test_readback_late.py`, `tests/test_tested_configurations.py`, `tests/test_vram_budget_row.py` and `tests/test_walk_key.py`. (With the default 32 checks at once, one or two small fixtures also hit their 10 s limit, a different one each run; each passes alone.) `tests/test_memory_priority.py` and `tests/test_blocks_at_start.py` pass. The renderer alone (`RX_VK_ONLY=1`, no game) built from 2e2e6490 on both systems, 0 files failed: on Windows 11 with llvm-mingw 20261104 (`longshot_vk.dll`, built without the DLSS SDK), and on Ubuntu 22.04 (under WSL 2) the way CI's "longshot_vk.so (build check)" step builds it (`RX_MARCH=x86-64-v3`, `RX_RELEASE=1`, the shipped profile; `longshot_vk.so`); there `tests/test_memory_priority.py`, `tests/test_blocks_at_start.py` and `tests/test_utf8_paths.py` pass too. In the game, on the build named in Baseline: the twelve runs above on the RTX 4090, Hyrule Field, DLSS on; every run's log has DLSS started and no device-lost line, and the frame time came back to 7.2 to 7.8 ms once the occupants were gone. With the switch on, the log says the three priorities. **Not tested:** this PR's head in the game (the runs' renderer is this PR's four files compiled on main @ 5e072fc8's tree; main's renderer has changed since, see Measurements, and the head's renderer was built but not run); a card with little VRAM of its own; AMD and Intel drivers; Linux in the game (built there, not run: no Linux with a GPU here); a device without the extension (the code path logs it and changes nothing; no such device here); the log line for a pair that isn't read (checked by `tests/test_memory_priority.py`: the rule, and that `vk_vma.cpp` logs it; not seen in the game); a scene that moves or loads; frame generation on; more than 5 minutes under pressure. **Must be tested:** - The PC of the runs, at this PR's head: Hyrule Field with `RX_VK_IMAGE_PREALLOC=9` and VRAM occupants until about 0.5 GiB of the game's memory is out of VRAM, `RX_VK_MEMORY_PRIORITY` unset then `=1`, expecting the `vk: memory priority` line at the start and a lower `vk: GPU time` with it on, as in the tables (by hand, the author: two interleaved runs each way, the `vk: GPU time` and `fps` lines) - Windows, an NVIDIA card with 8 GB or less: a heavy scene at a resolution that fills the VRAM, `RX_VK_MEMORY_PRIORITY` unset then `=1`, expecting the same or a better frame time with it on and no new stutter (by hand, test request T-MCA2: the `vk: GPU time`, `fps` and `vk: VRAM` lines of the log) - Windows, an AMD or Intel card: start the game with `RX_VK_MEMORY_PRIORITY=1` and play a few minutes, expecting the `vk: memory priority` line at the start and the frame time of the switch unset (by hand, test request T-ATFL: the log's `vk: memory priority` and `vk: GPU time` lines) - Linux, any GPU maker: start the game with `RX_VK_MEMORY_PRIORITY=1` and play a few minutes, expecting the `vk: memory priority` line at the start and the frame time of the switch unset (by hand, test request T-FKWJ: the same lines) The three requests wait for a maintainer's `testing wanted` label before a tester can take them. The last three decide the default more than the merge: with the switch unset, which is how it ships, nothing of this runs. ## Risks **What could break:** nothing with the switch unset, which is how it ships: the extension isn't asked for. With it on, a driver may ignore the priorities, or use them in a way that suits another scene less (textures brought back late could stutter where the camera turns fast; not seen standing still). The test form can do worse than nothing: `1,0.5` ended slower than the switch unset in its one run. **Turning it off:** unset `RX_VK_MEMORY_PRIORITY`; reverting the commit removes it. **Compatibility:** Nothing changes: the switch is unset by default, and no save, `longshot_options.ini` key, shader or pipeline cache is touched. `MkwVkHost` and `MkwVkRenderer` are as they were, so `MKWVK_API_VERSION` stays. ## Issues See #586 See #130 See #441 ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (Unset and `1`: 2 and 3 runs with 9 blocks, 2 and 2 with 24; the two test pairs have one run a pool size and are said so.) - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [x] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`). - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (The renderer built from 2e2e6490 on both, see Testing; the change handles no path.) - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(vk): RX_VK_MEMORY_PRIORITY tells the driver which memory to keep in VRAM when it runs short (off by default)
Some checks failed
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
cb44f43259
When other programs hold VRAM, or the card is too small for what is loaded, the system moves part of the game's
device memory out of VRAM and chooses what goes. With the switch on the renderer asks the device for
VK_EXT_memory_priority and gives each allocation a priority: the render targets' blocks 1, the sampled textures'
pool 0.25, the rest 0.5 (what all of it has without the switch). RX_VK_MEMORY_PRIORITY=<targets>,<textures> sets
the first two for a test. The rule is in gpu/vk/memory_priority.h, apart from Vulkan, with a game-free check.

Measured on one PC (RTX 4090, NVIDIA 621.24, Windows 11), standing in Hyrule Field while other processes took the
VRAM, twelve runs in docs/tested/2026-11-14-memory-priority-jiro113-*: with the textures' pool at an 8 GiB card's
9 blocks and about 0.5 GiB of the game's memory out of VRAM, 16.5 to 21.6 ms of GPU time a frame with the switch
against 30.6 and 33.6 ms without; with about 1.3 GiB out, 24 to 29 ms against 109 ms and more. The textures' lower
priority is what helps (0.5,0.25 did as well; 1,0.5 lost its gain within 90 s). No small card has run it yet, so
it stays off.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
First-time contributor

Pushed f9fa4490: the twelve docs/tested rows now name this pull request (nothing else changed since cb44f432). The description has the measurements, the log lines behind the 26624 MB step, and the renderer's build from cb44f432 on Windows (llvm-mingw) and Ubuntu 22.04. It stays WIP until the three test requests are open.

Pushed f9fa4490: the twelve docs/tested rows now name this pull request (nothing else changed since cb44f432). The description has the measurements, the log lines behind the 26624 MB step, and the renderer's build from cb44f432 on Windows (llvm-mingw) and Ubuntu 22.04. It stays WIP until the three test requests are open.
docs(tested): the memory priority runs name their pull request
Some checks failed
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
f9fa449073
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
First-time contributor

Opened the three test requests: T-MCA2 (an NVIDIA card with 8 GB or less on Windows), T-ATFL (AMD or Intel on Windows) and T-FKWJ (Linux, a run in the game). They wait for a maintainer's testing wanted label.

Opened the three test requests: T-MCA2 (an NVIDIA card with 8 GB or less on Windows), T-ATFL (AMD or Intel on Windows) and T-FKWJ (Linux, a run in the game). They wait for a maintainer's `testing wanted` label.
A review of the pull request found that <targets>,<textures> was read with atof and held to 0..1, so 0,5 (a half
written with a decimal comma) put the render targets at 0 and the textures at 1, the opposite of the rule, and nan
went through std::clamp to the driver as a priority. The pair is now two numbers from 0 to 1 and nothing else;
anything else with a comma gives the usual 1 and 0.25 and a line in the log.

The log's other line says the device has no VK_EXT_memory_priority "with its feature", which is the case it covers.
The check and the guide say what has a priority: what goes through VMA. Memory allocated with vkAllocateMemory (the
sparse textures' chunks, two small buffers, the imports of guest memory) and an image that lands in one of VMA's
shared blocks keep the driver's 0.5.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
docs(tested): the memory priority rows say what the renderer files gained since the runs
Some checks failed
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 3s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
706c01bb84
The Build cell said the header had gained a comment and nothing else. Since
then it has also gained the pair's strict reading, and vk_vma.cpp two log
lines; the values that were run read as before.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
First-time contributor

Two commits (99ec88a8, 706c01bb) after a second AI agent, started with nothing from the work on this change, read the PR. What changed and why:

  • The pair is read strictly. RX_VK_MEMORY_PRIORITY=<targets>,<textures> made priorities out of 0,5 (meant as a half), nan,0.25, 1, or three numbers. It now takes two numbers from 0 to 1 and nothing else; anything else with a comma in it gives the usual 1 and 0.25, and a line in the log says the value wasn't read. The values that were measured (1, 0.5,0.25, 1,0.5) read as before. tests/test_memory_priority.py has the refused cases.
  • "Every allocation" was too wide. It holds for what goes through VMA. The sparse textures' chunks, two small buffers and the imports of guest memory come from vkAllocateMemory and keep the driver's 0.5, and so does an image that overflows its pool into one of VMA's shared blocks. The guide, the check and the description now say so; the code there is unchanged, and a priority for the sparse chunks is left for a follow-up.
  • A log line said the wrong thing. "The device has no VK_EXT_memory_priority" also covered a device with the extension and not its feature: reworded.
  • The single runs were stated too firmly. 0.5,0.25 and 1,0.5 have one run each a pool size; the description and the header's comment now say "looks like", and that 1,0.5 ended slower than the switch unset.
  • The twelve docs/tested rows say what the renderer files gained since the runs.

Checked again: tests/run_all.py at 706c01bb (522 passed, 43 skipped, and the same 7 failures as on main), and the renderer built from 99ec88a8 on Windows and on Ubuntu 22.04, 0 files failed. The head has still not run in the game, so the numbers are those of the measured build. Still WIP.

Two commits (99ec88a8, 706c01bb) after a second AI agent, started with nothing from the work on this change, read the PR. What changed and why: - **The pair is read strictly.** `RX_VK_MEMORY_PRIORITY=<targets>,<textures>` made priorities out of `0,5` (meant as a half), `nan,0.25`, `1,` or three numbers. It now takes two numbers from 0 to 1 and nothing else; anything else with a comma in it gives the usual 1 and 0.25, and a line in the log says the value wasn't read. The values that were measured (`1`, `0.5,0.25`, `1,0.5`) read as before. `tests/test_memory_priority.py` has the refused cases. - **"Every allocation" was too wide.** It holds for what goes through VMA. The sparse textures' chunks, two small buffers and the imports of guest memory come from `vkAllocateMemory` and keep the driver's 0.5, and so does an image that overflows its pool into one of VMA's shared blocks. The guide, the check and the description now say so; the code there is unchanged, and a priority for the sparse chunks is left for a follow-up. - **A log line said the wrong thing.** "The device has no VK_EXT_memory_priority" also covered a device with the extension and not its feature: reworded. - **The single runs were stated too firmly.** `0.5,0.25` and `1,0.5` have one run each a pool size; the description and the header's comment now say "looks like", and that `1,0.5` ended slower than the switch unset. - The twelve `docs/tested` rows say what the renderer files gained since the runs. Checked again: `tests/run_all.py` at 706c01bb (522 passed, 43 skipped, and the same 7 failures as on main), and the renderer built from 99ec88a8 on Windows and on Ubuntu 22.04, 0 files failed. The head has still not run in the game, so the numbers are those of the measured build. Still WIP.
Jiro113 changed title from WIP: feat(vk): RX_VK_MEMORY_PRIORITY tells the driver which memory to keep in VRAM when it runs short (off by default) to feat(vk): RX_VK_MEMORY_PRIORITY tells the driver which memory to keep in VRAM when it runs short (off by default) 2026-11-14 19:53:55 +00:00
Author
First-time contributor

Also, not sure about the proper process but reviews are requested, please !

Also, not sure about the proper process but reviews are requested, please !
Jiro113 force-pushed feat/vk-memory-priority from 706c01bb84
Some checks failed
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 3s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
to 2e2e649085
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 17s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 22:00:56 +00:00
Compare
Author
First-time contributor

Rebased onto main @ 82886e89 (the head is now 2e2e6490, was 706c01bb) and the description moved to the new template. What changed and why:

  • The conflict was in tests/test_blocks_at_start.py, where main added the integrated case to the list this PR adds priority and priority-compaction-off to: all three are kept. Nothing else conflicted, and the four commits are otherwise as they were (git range-diff shows that one line).
  • The description follows the new template: a status line, Why with the issues that fit (#586, #130, #441 found by searching the tracker), the verdict, the tables' columns named by build and commit, Flags and settings, what must still be tested, and Risks. The numbers and the log excerpt are the same as before.
  • Not measured again. The runs are from main @ 5e072fc8 with this PR's renderer files, and main is 29 commits further. Why the comparison still holds, and what doesn't carry over, is in Measurements under "Since the runs": main's renderer changes since (the render thread, the play profile, the integrated-GPU texture budget, an Intel Arc case) add no pool or allocation and set no priority, so on a dedicated card the switch sets the same priorities on the same memory; the frame times themselves are the measured build's, and the gain at this head is still to be seen. A run at this head on the same PC is the first item of Must be tested.

Checked again at 2e2e6490: the renderer builds on Windows (llvm-mingw) and on Ubuntu 22.04 the way CI's build check does, 0 files failed; tests/test_memory_priority.py and tests/test_blocks_at_start.py pass on both. tests/run_all.py --jobs 12 on Windows: 521 passed, 14 failed, 43 skipped, and the fourteen fail the same way on main @ 82886e89 here (named in Testing).

The three test requests (T-MCA2, T-ATFL, T-FKWJ) still wait for a maintainer's testing wanted label, and the workflows for a maintainer's approval.

Rebased onto main @ 82886e89 (the head is now 2e2e6490, was 706c01bb) and the description moved to the new template. What changed and why: - **The conflict** was in `tests/test_blocks_at_start.py`, where main added the `integrated` case to the list this PR adds `priority` and `priority-compaction-off` to: all three are kept. Nothing else conflicted, and the four commits are otherwise as they were (`git range-diff` shows that one line). - **The description follows the new template**: a status line, Why with the issues that fit (#586, #130, #441 found by searching the tracker), the verdict, the tables' columns named by build and commit, Flags and settings, what must still be tested, and Risks. The numbers and the log excerpt are the same as before. - **Not measured again.** The runs are from main @ 5e072fc8 with this PR's renderer files, and main is 29 commits further. Why the comparison still holds, and what doesn't carry over, is in Measurements under "Since the runs": main's renderer changes since (the render thread, the play profile, the integrated-GPU texture budget, an Intel Arc case) add no pool or allocation and set no priority, so on a dedicated card the switch sets the same priorities on the same memory; the frame times themselves are the measured build's, and the gain at this head is still to be seen. A run at this head on the same PC is the first item of Must be tested. Checked again at 2e2e6490: the renderer builds on Windows (llvm-mingw) and on Ubuntu 22.04 the way CI's build check does, 0 files failed; `tests/test_memory_priority.py` and `tests/test_blocks_at_start.py` pass on both. `tests/run_all.py --jobs 12` on Windows: 521 passed, 14 failed, 43 skipped, and the fourteen fail the same way on main @ 82886e89 here (named in Testing). The three test requests (T-MCA2, T-ATFL, T-FKWJ) still wait for a maintainer's `testing wanted` label, and the workflows for a maintainer's approval.
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 17s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'feat/vk-memory-priority:Jiro113-feat/vk-memory-priority'
git switch 'Jiro113-feat/vk-memory-priority'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!560
No description provided.