perf(vk): make part of the image pool's blocks at the start #2438

Merged
lucasmazzoni45-del merged 1 commit from perf-image-pool-prealloc into main 2026-11-12 03:04:48 +00:00
lucasmazzoni45-del commented 2026-11-11 19:15:58 +00:00 (Migrated from github.com)

Each 128 MB block of the images' pool is a vkAllocateMemory on the replay thread, ~170 ms on AMD's Windows driver. A walk across Hyrule Field on an RX 7800 XT made ~70 of them and the frame rate dropped to the low 40s each time new ground came in. The pool now starts with 15% of the device-local memory as blocks (2 to 24; 19 on a 16 GB card). RX_VK_IMAGE_PREALLOC=<blocks> sets the count, 0 keeps the old behaviour; if the blocks do not fit the pool is made empty as before.

Scripted 1080p walk, same route, measured on older trees together with other changes (v2.0.5-v2.0.12): mean fps 52.9 -> 59.8, min 41.6 -> 56.7, replay submit 14.5 -> 9.9 ms. I have not re-measured this change alone on v2.1.0. Log line on start: images from a pool of 128 MB blocks (memory type 0), 19 made at the start. Costs VRAM up front (about 2.4 GB on this card), so a card with little free memory may want a lower value.

Tested: the Vulkan back end builds (llvm-mingw, RX_VK_ONLY) on top of v2.1.0, and a full build of that tree boots to the title at 60 fps on an RX 7800 XT / Ryzen 7 5800X3D (Windows 11, driver 0x8001a0) with the new log lines present. The repository test suite was not run, and I have not yet played a long session on this build, so the effect in real play is not shown. Related data: #995.

🤖 Generated with Claude Code

Each 128 MB block of the images' pool is a `vkAllocateMemory` on the replay thread, ~170 ms on AMD's Windows driver. A walk across Hyrule Field on an RX 7800 XT made ~70 of them and the frame rate dropped to the low 40s each time new ground came in. The pool now starts with 15% of the device-local memory as blocks (2 to 24; 19 on a 16 GB card). `RX_VK_IMAGE_PREALLOC=<blocks>` sets the count, `0` keeps the old behaviour; if the blocks do not fit the pool is made empty as before. Scripted 1080p walk, same route, measured on older trees together with other changes (v2.0.5-v2.0.12): mean fps 52.9 -> 59.8, min 41.6 -> 56.7, replay submit 14.5 -> 9.9 ms. I have not re-measured this change alone on v2.1.0. Log line on start: `images from a pool of 128 MB blocks (memory type 0), 19 made at the start`. Costs VRAM up front (about 2.4 GB on this card), so a card with little free memory may want a lower value. **Tested:** the Vulkan back end builds (llvm-mingw, `RX_VK_ONLY`) on top of v2.1.0, and a full build of that tree boots to the title at 60 fps on an RX 7800 XT / Ryzen 7 5800X3D (Windows 11, driver 0x8001a0) with the new log lines present. The repository test suite was **not** run, and I have not yet played a long session on this build, so the effect in real play is not shown. Related data: #995. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Joey-Magnus (Migrated from github.com) requested changes 2026-11-11 20:08:47 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Thanks for chasing the vkAllocateMemory stalls on AMD's Windows driver; making blocks up front is a sensible way to take them off the replay thread. The fallback when the pool can't be made with its blocks is handled correctly, and the checks that read vk_vma.cpp (test_target_image_pool, test_vram_compact, test_texture_staging_publication) pass at this head. The block count, though, is sized from the heap's total size rather than what the process may actually use, and it lands badly on two configurations the repo already has special handling for. Details:

blocking gpu/vk/vk_vma.cpp:103-109: the blocks are made without regard to the memory budget. VMA's CreateMinBlocks goes straight to vkAllocateMemory (it only honours a heap size limit, never VK_EXT_memory_budget), whereas every image create_image makes asks for VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT. When other programs already hold VRAM (the tested-configurations table has a 12 GB card with only 10.2-10.5 GB allowed), 2.4 GB made up front can push the process over what the driver allows. On Windows that doesn't fail the allocation: WDDM pages it out, which is the "GPU time 13 → 55 ms for good" failure the texture budget's comments in vk_texture.cpp describe. Please cap n by the heap's free budget (vmaGetHeapBudgets: budget - usage, with some margin), not by memoryHeaps[h].size.

blocking gpu/vk/vk_vma.cpp:103-106: the heap is the largest device-local heap, not the heap of type (dev.memory.memoryTypes[type].heapIndex), and integrated GPUs aren't excluded. On UMA drivers the device-local heap is a share of system RAM, so a 32 GB laptop could pin up to 24 × 128 MB = 3 GB of RAM at the first image. That is more than the 2560 MB textures are capped at on an integrated GPU (vk_texture.cpp, the 780M laptop that ran out of memory), and the 2-block minimum alone takes a quarter of a Steam Deck's default 1 GB carve-out. I'd skip pre-making on VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU unless RX_VK_IMAGE_PREALLOC asks for it, and use the type's heap.

should fix: interaction with RX_VK_COMPACT=1. With compaction on, textures go to texture_pool (create_image, textures && textures_pool()), so images_pool keeps only DLSS's, frame interpolation's and scratch images. The pre-made blocks then sit mostly empty for the whole session (VMA never frees blocks below minBlockCount). Under compaction the texture budget only subtracts the textures' pool's free space (vram_compact::reclaimable(block_free, pool_free)), so those empty blocks count as pressure and the held budget evicts roughly that much of the textures. The two features work against each other: compaction exists to give blocks back. Pre-make nothing (or far fewer) when compaction is on, or pre-make in the pool the textures actually use.

question: the numbers (52.9 → 59.8 mean, 41.6 → 56.7 min) come from v2.0.5-v2.0.12 trees together with other changes. Since #1436 (in v2.0.12), render targets have their own pool (target_images_pool), and this change doesn't pre-make blocks there, so some of the ~70 allocations you counted may no longer be in images_pool at all. Could you re-run the Hyrule Field walk on current main with RX_VK_IMAGE_PREALLOC=0 vs. the default and post the vk: image blocks log lines? That would show how many blocks each pool really grows to. The PR also says the test suite wasn't run, and only the title check has run in CI here.

minor gpu/vk/vk_vma.cpp:109: the first image now pays for all the blocks at once on the replay thread (19 × ~170 ms ≈ 3 s on your card, per your own timing). That's probably fine while booting, but worth saying in the log line, and worth checking it can't land mid-gameplay (for example on the first image after a device-lost recovery, if the pools are re-made).

Thanks for chasing the `vkAllocateMemory` stalls on AMD's Windows driver; making blocks up front is a sensible way to take them off the replay thread. The fallback when the pool can't be made with its blocks is handled correctly, and the checks that read `vk_vma.cpp` (`test_target_image_pool`, `test_vram_compact`, `test_texture_staging_publication`) pass at this head. The block count, though, is sized from the heap's total size rather than what the process may actually use, and it lands badly on two configurations the repo already has special handling for. Details: **blocking** `gpu/vk/vk_vma.cpp:103-109`: the blocks are made without regard to the memory budget. VMA's `CreateMinBlocks` goes straight to `vkAllocateMemory` (it only honours a heap size limit, never `VK_EXT_memory_budget`), whereas every image `create_image` makes asks for `VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT`. When other programs already hold VRAM (the tested-configurations table has a 12 GB card with only 10.2-10.5 GB allowed), 2.4 GB made up front can push the process over what the driver allows. On Windows that doesn't fail the allocation: WDDM pages it out, which is the "GPU time 13 → 55 ms for good" failure the texture budget's comments in `vk_texture.cpp` describe. Please cap `n` by the heap's free budget (`vmaGetHeapBudgets`: `budget - usage`, with some margin), not by `memoryHeaps[h].size`. **blocking** `gpu/vk/vk_vma.cpp:103-106`: the heap is the *largest* device-local heap, not the heap of `type` (`dev.memory.memoryTypes[type].heapIndex`), and integrated GPUs aren't excluded. On UMA drivers the device-local heap is a share of system RAM, so a 32 GB laptop could pin up to 24 × 128 MB = 3 GB of RAM at the first image. That is more than the 2560 MB textures are capped at on an integrated GPU (`vk_texture.cpp`, the 780M laptop that ran out of memory), and the 2-block minimum alone takes a quarter of a Steam Deck's default 1 GB carve-out. I'd skip pre-making on `VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU` unless `RX_VK_IMAGE_PREALLOC` asks for it, and use the type's heap. **should fix**: interaction with `RX_VK_COMPACT=1`. With compaction on, textures go to `texture_pool` (`create_image`, `textures && textures_pool()`), so `images_pool` keeps only DLSS's, frame interpolation's and scratch images. The pre-made blocks then sit mostly empty for the whole session (VMA never frees blocks below `minBlockCount`). Under compaction the texture budget only subtracts the textures' pool's free space (`vram_compact::reclaimable(block_free, pool_free)`), so those empty blocks count as pressure and the held budget evicts roughly that much of the textures. The two features work against each other: compaction exists to give blocks back. Pre-make nothing (or far fewer) when compaction is on, or pre-make in the pool the textures actually use. **question**: the numbers (52.9 → 59.8 mean, 41.6 → 56.7 min) come from v2.0.5-v2.0.12 trees *together with other changes*. Since #1436 (in v2.0.12), render targets have their own pool (`target_images_pool`), and this change doesn't pre-make blocks there, so some of the ~70 allocations you counted may no longer be in `images_pool` at all. Could you re-run the Hyrule Field walk on current main with `RX_VK_IMAGE_PREALLOC=0` vs. the default and post the `vk: image blocks` log lines? That would show how many blocks each pool really grows to. The PR also says the test suite wasn't run, and only the title check has run in CI here. **minor** `gpu/vk/vk_vma.cpp:109`: the first image now pays for all the blocks at once on the replay thread (19 × ~170 ms ≈ 3 s on your card, per your own timing). That's probably fine while booting, but worth saying in the log line, and worth checking it can't land mid-gameplay (for example on the first image after a device-lost recovery, if the pools are re-made).
Joey-Magnus (Migrated from github.com) requested changes 2026-11-11 21:24:13 +00:00
Joey-Magnus (Migrated from github.com) left a comment

The earlier review is still open, and I'd add:

  • vk_vma.cpp:103-109 makes up to 24 x 128 MB blocks sized from the largest device-local heap. VMA's minimum blocks skip WITHIN_BUDGET, and that heap isn't necessarily type's.
  • Integrated GPUs aren't excluded, so the blocks come out of system RAM: up to 3 GB on a laptop, and 256 MB of a Steam Deck's 1 GB carve-out.
  • With RX_VK_COMPACT=1, textures go to the other pool, so these blocks sit empty and count as pressure.
The earlier review is still open, and I'd add: - `vk_vma.cpp:103-109` makes up to 24 x 128 MB blocks sized from the largest device-local heap. VMA's minimum blocks skip `WITHIN_BUDGET`, and that heap isn't necessarily `type`'s. - Integrated GPUs aren't excluded, so the blocks come out of system RAM: up to 3 GB on a laptop, and 256 MB of a Steam Deck's 1 GB carve-out. - With `RX_VK_COMPACT=1`, textures go to the other pool, so these blocks sit empty and count as pressure.
hckrman101 (Migrated from github.com) reviewed 2026-11-11 22:19:30 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks for measuring the allocation cost on AMD's Windows driver. I agree with Joey-Magnus's two blocking points (the blocks are made outside the memory budget, and the heap is the largest device-local one rather than type's, with integrated GPUs not excluded), and they're still open at this head. One thing makes his compaction point weigh more than it did:

Compaction is on by default on main now. The branch is based on v2.1.0, where it was opt-in (RX_VK_COMPACT=1). On current main vram_compact::enabled() is true unless RX_VK_COMPACT=0, so create_image sends sampled textures to textures_pool() and render targets to target_images_pool() (#1436). What's left in images_pool is DLSS's, frame interpolation's and the scratch images. So in the default configuration this pre-makes ~2.4 GB (on a 16 GB card) in a pool that mostly stays empty, VMA never gives back blocks under minBlockCount, and the texture budget's hold only discounts the textures' pool's free space (reclaimable(block_free, pool_free)), so those empty blocks count as pressure and push textures out. The ~70 allocations in the Hyrule Field walk were very likely texture blocks, which are now in a different pool. If pre-making helps, it probably belongs on texture_pool (with compaction's give-back in mind) or should be sized from what images_pool actually grows to. The vk: image blocks stats line on current main, RX_VK_IMAGE_PREALLOC=0 against the default, would answer that, as Joey-Magnus asked.

Overlap with your sibling PRs: #2437 also holds more memory up front (capture spares up to 768 MB of host-visible memory instead of 64 MB, made ahead on a worker). The two add up on a low-VRAM or integrated machine, so it'd help to size both against the budget the same way. #2440 (worker count) touches only vk_jobs.cpp, so there's no overlap. None of the three conflict at file level.

Smaller points:

  • nit: RX_VK_IMAGE_PREALLOC has no upper bound (atoi of a large value asks for that many 128 MB blocks). Clamping it like RX_VK_WORKERS in #2440 would keep a typo from failing the pool.
  • Only the title check has run here, and the description says the test suite wasn't run. test_target_image_pool, test_vram_compact and test_texture_staging_publication read this file. Joey-Magnus reports they pass at this head, but a run on a branch rebased onto current main would be good.

I'm leaving this as a comment. It needs the budget/heap/iGPU fixes and a re-measure on current main (with compaction on) before it can go in.

Thanks for measuring the allocation cost on AMD's Windows driver. I agree with Joey-Magnus's two blocking points (the blocks are made outside the memory budget, and the heap is the largest device-local one rather than `type`'s, with integrated GPUs not excluded), and they're still open at this head. One thing makes his compaction point weigh more than it did: **Compaction is on by default on main now.** The branch is based on v2.1.0, where it was opt-in (`RX_VK_COMPACT=1`). On current main `vram_compact::enabled()` is true unless `RX_VK_COMPACT=0`, so `create_image` sends sampled textures to `textures_pool()` and render targets to `target_images_pool()` (#1436). What's left in `images_pool` is DLSS's, frame interpolation's and the scratch images. So in the default configuration this pre-makes ~2.4 GB (on a 16 GB card) in a pool that mostly stays empty, VMA never gives back blocks under `minBlockCount`, and the texture budget's hold only discounts the textures' pool's free space (`reclaimable(block_free, pool_free)`), so those empty blocks count as pressure and push textures out. The ~70 allocations in the Hyrule Field walk were very likely texture blocks, which are now in a different pool. If pre-making helps, it probably belongs on `texture_pool` (with compaction's give-back in mind) or should be sized from what `images_pool` actually grows to. The `vk: image blocks` stats line on current main, `RX_VK_IMAGE_PREALLOC=0` against the default, would answer that, as Joey-Magnus asked. Overlap with your sibling PRs: #2437 also holds more memory up front (capture spares up to 768 MB of host-visible memory instead of 64 MB, made ahead on a worker). The two add up on a low-VRAM or integrated machine, so it'd help to size both against the budget the same way. #2440 (worker count) touches only `vk_jobs.cpp`, so there's no overlap. None of the three conflict at file level. Smaller points: - nit: `RX_VK_IMAGE_PREALLOC` has no upper bound (`atoi` of a large value asks for that many 128 MB blocks). Clamping it like `RX_VK_WORKERS` in #2440 would keep a typo from failing the pool. - Only the title check has run here, and the description says the test suite wasn't run. `test_target_image_pool`, `test_vram_compact` and `test_texture_staging_publication` read this file. Joey-Magnus reports they pass at this head, but a run on a branch rebased onto current main would be good. I'm leaving this as a comment. It needs the budget/heap/iGPU fixes and a re-measure on current main (with compaction on) before it can go in.
@ -103,0 +106,4 @@
uint64_t n = std::clamp<uint64_t>(heap / 100 * 15 / kImageBlock, 2, 24);
if (const char* e = getenv("RX_VK_IMAGE_PREALLOC")) n = (uint64_t)std::max(0, atoi(e));
pi.minBlockCount = (size_t)n;
if (n && vmaCreatePool(vma, &pi, &image_pool) != VK_SUCCESS) { // (no room for them: as before, made as needed)
hckrman101 (Migrated from github.com) commented 2026-11-11 22:19:30 +00:00

On current main, with compaction on by default, textures don't land in this pool any more (they go to textures_pool()), so these blocks mostly hold nothing and stay allocated for the whole session (VMA keeps minBlockCount blocks). I'd size this from what the pool actually needs on main, and against vmaGetHeapBudgets for dev.memory.memoryTypes[type].heapIndex, not from the largest heap's total size.

On current main, with compaction on by default, textures don't land in this pool any more (they go to `textures_pool()`), so these blocks mostly hold nothing and stay allocated for the whole session (VMA keeps `minBlockCount` blocks). I'd size this from what the pool actually needs on main, and against `vmaGetHeapBudgets` for `dev.memory.memoryTypes[type].heapIndex`, not from the largest heap's total size.
Owner

@blushOFF commented on GitHub · 2026-11-12 06:24 UTC · original

On an 8 GB card (RTX 3070, Windows) the blocks made at the start cost a lot in Castle Town Market: 31-36 fps on main against 54-56 with RX_VK_IMAGE_PREALLOC=0, because the texture budget drops to ~0.9 GB while the blocks hold ~1 GB free. Numbers and logs' lines in #2690.

> [@blushOFF](https://github.com/blushOFF) commented on GitHub · 2026-11-12 06:24 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2438#issuecomment-6816416633) On an 8 GB card (RTX 3070, Windows) the blocks made at the start cost a lot in Castle Town Market: 31-36 fps on main against 54-56 with `RX_VK_IMAGE_PREALLOC=0`, because the texture budget drops to ~0.9 GB while the blocks hold ~1 GB free. Numbers and logs' lines in #2690. <!-- github-sync:c:6816416633 -->
Commenting is not possible because the repository is archived.
No description provided.