Skip to content

fix(agc): place device memory in host memory when the driver refuses a device-local allocation - #5987

Merged
adriadam10 merged 6 commits into
boykopovar:mainfrom
GustavoGamarra95:fix/agc-device-memory-host-fallback
Oct 18, 2026
Merged

adriadam10 merged 6 commits into
boykopovar:mainfrom
GustavoGamarra95:fix/agc-device-memory-host-fallback

Conversation

@GustavoGamarra95

@GustavoGamarra95 GustavoGamarra95 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

DeviceMemoryType places a device-local allocation in host memory only when VK_EXT_memory_budget reports the heap as full. The report lags the driver. On a 4 GB RTX 3050 Laptop the heap was at 3684 of a 3685 MiB budget and the budget still showed room. The NVIDIA driver refused the allocation with VK_ERROR_OUT_OF_DEVICE_MEMORY, and the title stopped:

[videoout] presentation failed: AGC graphics: vkAllocateMemory device buffer: allocation=9437184 memoryType=1 reclaimed=604900608 initialResult=-2 attempts=2: Vulkan result -2

With device buffers covered, the next run stopped the same way at vkAllocateMemory depth target.

This is mostly a Linux/NVIDIA problem. On Windows, WDDM demotes allocations to system memory itself instead of refusing them, so these paths should rarely run there. I have not tested Windows.

Host placement is the last step of BufferPool::AllocateMemory, after the recovery that is already on main:

  1. trim the pool's idle tiers and retry device-local memory;
  2. release the sampled textures and clean storage images through the reclaim hook from fix(agc): release the texture caches when a device allocation runs out of memory #6174 and retry device-local memory;
  3. only if that retry is refused too, allocate in a host memory type the requirements allow.

A refusal the reclaim can satisfy never reaches host placement.

Changes:

  • fix: a device buffer still refused after steps 1 and 2 is allocated in host memory. It is marked host-placed, so the device pool does not keep it after release.

  • fix: every image allocation goes through the same path (AllocateImageMemory). This covers textures, storage textures, block, repacked, depth, depth plane and stencil copies, render, depth and multisampled targets, and attachment proxies. Several of these sites called vkAllocateMemory directly and stopped the title on the first refusal: the render, depth and multisampled targets, the attachment proxies, and the block, repacked, depth plane and stencil copies. The 1x1 denormal clear probe is left as it is.

  • Only VK_ERROR_OUT_OF_DEVICE_MEMORY from a device-local type falls back. Other results, and requirements that allow no host type, fail as before.

  • Each placement is logged with the bytes trimmed and released and the cumulative MiB, rate-limited to powers of two like the existing budget placement.

  • docs: docs/dev/debt/libSceAgcDriver.md records that a placed image keeps its placement until it is destroyed and is never moved back, so it is slower (PCIe) for its lifetime.

  • test: agc_buffer_pool_tests covers these cases:

    • RefusedDevicePlacement: a device buffer refused within the reported budget must land in host memory and leave the pool on release; a context with no host type must still fail.
    • AttachmentReclaim: an attachment refused once must reclaim the pool and stay device-local; refused twice, it must land in host memory.
    • ImageCacheReclaim: a texture refused once must release the texture caches and stay device-local; refused again, it must land in host memory.

    Each fails without its fix. agc_device_memory_fallback_tests covers the choice of fallback type: a refused device-local type, a refused host type, the type mask, and an unknown type.

Overlaps #4472 in Context.hpp and tests/BufferPool.cpp. That PR stops pooling released device buffers over the budget; this one handles a refusal the budget did not predict. Whichever merges second needs a manual merge there. pr_overlap also reports line overlaps with #7119 (tests/BufferPool.cpp) and #6764 and #7256 (DepthSurface.cpp). #6553 is stacked on this PR.

Tested

Rebased onto main 9f25d0b (head 9f98c7c). Linux (CachyOS, kernel 7.2.9), Release build:

Test RTX 3050 Laptop 615.78.08 lavapipe (Mesa 26.2.4)
agc_buffer_pool, _small_device_shared, _small_device_no_staging pass pass
agc_device_memory_fallback pass pass
agc_driver_graphics pass pass
agc_driver_covered_centroid_state pass pass
agc_driver_tessellation pass skipped
agc_driver_depth_storage_reuse pass skipped
all 700 agc_* ctests pass –

Beast of Reincarnation (PPSA29343), RTX 3050 Laptop 4 GB, 900 s per run with the same scripted pad input. These runs used head 8bb3e07, which lacks only the repacked/stencil copy routing, the MiB counter and the DeviceBuffer cleanup:

Window this PR, warm cache: FPS / p50 main (bd58561), two warm runs: FPS / p50
0–200 s 8.0 / 192 ms 9.0 / 173 ms, 8.0 / 147 ms
200–400 s 0.4 / 1430 ms 0.6 / 1518 ms, 0.7 / 1395 ms
400–600 s 0.5 / 1435 ms 0.7 / 1461 ms, 0.7 / 1376 ms
600–900 s 0.9 / 1020 ms – (600 s runs)

Both runs reached 900 s with no stop, no fault and no device lost. Peak VRAM was 3480 MiB, the same as main. The driver refused no allocation in these runs, so the host placement path did not run there: they show the rebase costs nothing, not what a placement costs. The run with a cold shader cache (2617 misses) is left out of the table.

Earlier data, before #6174 merged (head 4afec57 and older):

  • New Game on main stopped at the device buffer refusal after about 15 minutes. With only the buffer fallback, the next run stopped at the depth target refusal. With this PR the runs stayed up for the full 15–24 minutes measured.
  • Per 600 s run, the textures that landed in host memory were 41–44 MiB and the storage textures 0–2 MiB, all on their first refusal, which this head no longer allows. No attachment reached host memory once the pool reclaim was in place.
  • 200–600 s ran at 1.11 and 1.18 FPS with the attachment reclaim, against 1.09 FPS without it. The title is CPU-bound here (one recording thread near 100%).

Checklist

@github-actions github-actions Bot added the agc libSceAgc* label Oct 14, 2026

@CnRJay CnRJay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Falling back on VK_ERROR_OUT_OF_DEVICE_MEMORY instead of stopping is reasonable for buffers. They are pooled and short-lived, and a host-placed one leaves the pool on release, so the cost goes away with it.

For images it is different, because a host-placed image keeps its placement until it is destroyed. If a refusal hits during a short spike, a long-lived render target or depth target lands in system memory and stays there for the rest of the session. Every draw that writes or tests against it then goes over PCIe, at a fraction of VRAM bandwidth. On a 4 GB laptop part that is a large, permanent frame-time loss that no log line ties to the spike. Render targets, depth targets, multisampled targets and attachment proxies are the worst candidates for host memory. Sampled textures and block/depth copies are much more tolerable.

Some suggestions:

  • Reclaim before falling back for images too. The buffer path reclaims the pool and retries device-local first, but the image paths go straight to host memory. Evicting from the sampled and storage image caches (and the buffer pool) and retrying device-local once would avoid most of these placements, since the caches are usually what fills the heap.
  • Keep attachments device-local. For render, depth and multisampled targets, it may be better to keep the current failure, or to fall back only after a reclaim fails, and log that as the hard limit.
  • Show the cost. The PR says about 2 GB ended up in host memory in Beast of Reincarnation but gives no frame times. The summary lines for a stretch of that run with and without placements (or with the cache budgets reduced so main doesn't hit the refusal) would show what the title pays.

This is mostly a Linux/NVIDIA problem: on Windows, WDDM demotes allocations itself instead of refusing them. A sentence in the description saying so would help reviewers on the other platform.

@ee6aba54

Copy link
Copy Markdown
Contributor

#6174 touches the same allocation sites (it releases the texture caches before the pool's retry). I will put it behind this one and route its texture and storage image allocations through Context::AllocateDeviceMemory once this is merged, so no change is needed here.

@GustavoGamarra95

Copy link
Copy Markdown
Contributor Author

Thanks @CnRJay and @ee6aba54 for the feedback! I really appreciate you both taking the time to look into this.

I'll spend some time today going through the points you raised and figuring out the best way to address them.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 4472 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp L308-313
core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp L169-179, L209, L790-791
PR 7119 core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp L177-179, L695
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp L253-261

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1368 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1395 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 2434 docs/dev/debt/libSceAgcDriver.md
PR 4072 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4650 docs/dev/debt/libSceAgcDriver.md
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5499 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5925 docs/dev/debt/libSceAgcDriver.md
PR 6177 core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6273 docs/dev/debt/libSceAgcDriver.md
PR 6280 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6292 core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6297 core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp
PR 6300 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6320 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6412 docs/dev/debt/libSceAgcDriver.md
PR 6414 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6423 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 6472 docs/dev/debt/libSceAgcDriver.md
PR 6481 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6490 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6529 docs/dev/debt/libSceAgcDriver.md
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6563 docs/dev/debt/libSceAgcDriver.md
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6741 docs/dev/debt/libSceAgcDriver.md
PR 6763 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 6786 docs/dev/debt/libSceAgcDriver.md
PR 6808 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6868 docs/dev/debt/libSceAgcDriver.md
PR 6941 docs/dev/debt/libSceAgcDriver.md
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7008 docs/dev/debt/libSceAgcDriver.md
PR 7201 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 7390 docs/dev/debt/libSceAgcDriver.md
PR 7426 docs/dev/debt/libSceAgcDriver.md
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md

@ee6aba54

Copy link
Copy Markdown
Contributor

Point 4 (textures and storage textures going through the pool reclaim and a device-local retry first) is what #6174 does: texture, depth copy and storage texture allocate through BufferPool::AllocateMemory, which trims the pool's idle tiers, releases sampled textures and clean storage images through a reclaim hook, and retries before it reports the failure. Both PRs change the same three vkAllocateMemory lines in Texture.cpp, so one of them will have to rebase on the other.

A possible order: #6174 first (it merges cleanly with current main and does not need this PR), then this PR places memory in host memory only when the pool's reclaim and retry still fail, e.g. as the last step inside AllocateMemory or in an AllocateDeviceMemory called by it. That covers the 0.12 to 0.03 FPS case, because a refusal that the reclaim can satisfy never reaches host placement. Happy to rebase #6174 onto whatever order you and #6553 agree on.

@GustavoGamarra95

Copy link
Copy Markdown
Contributor Author

Thanks @adriadam10 for the review, and @CnRJay, @ee6aba54, @SleglJan and @parl0v for the runs, data and corrections on this PR; they made the remaining work much clearer. Sorry for the delay in getting back to this.

Rebased onto main 922dc89:

AI-assisted: yes

@GustavoGamarra95

Copy link
Copy Markdown
Contributor Author

Rebased onto main 2626e5e, which carries #6174 (head b3187ec).

Host placement is now the last step of BufferPool::AllocateMemory, after the pool's own recovery and #6174's cache release:

  1. trim the pool's idle tiers and retry device-local memory;
  2. release the sampled textures and clean storage images through cacheReclaim and retry device-local memory;
  3. only if that retry is refused too, allocate in a host memory type the requirements allow. The log line now carries both the bytes trimmed and the bytes released.

A refusal that the reclaim can satisfy never reaches host placement, which covers the textures and storage textures point from @adriadam10's review.

The new commit also routes every image allocation through one helper, AllocateImageMemory (renamed from AllocateAttachmentMemory). That covers textures, storage textures, block, depth and depth plane copies, render, depth and multisampled targets, and attachment proxies. Block copies and depth plane copies were not covered by #6174 and still fell back on their first refusal. They now take the same path. Context::AllocateDeviceMemory is gone.

Tests, Release build, Linux 7.2.9:

Test RTX 3050 Laptop 615.78.08 lavapipe
agc_buffer_pool (+ small_device_shared, small_device_no_staging) pass pass
agc_device_memory_fallback pass pass
agc_driver_graphics pass pass
agc_driver_covered_centroid_state pass pass
agc_driver_tessellation pass skipped
agc_driver_depth_storage_reuse pass skipped
all agc_* ctests 696/696 pass –

New ImageCacheReclaim case in agc_buffer_pool_tests:

  • a texture refused device memory releases the texture caches and stays device-local when that makes room;
  • it lands in host memory only when the retry is refused again.

It fails when the cache release is skipped before host placement.

Correction to my earlier reply: the 007 First Light data came from @SleglJan, not @CnRJay. Sorry about the mix-up.

AI-assisted: yes

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@GustavoGamarra95
GustavoGamarra95 force-pushed the fix/agc-device-memory-host-fallback branch from b3187ec to 9f98c7c Compare October 18, 2026 11:19
@GustavoGamarra95

Copy link
Copy Markdown
Contributor Author

Rebased onto main 9f25d0b (head 9f98c7c). A commit merged since the last rebase added a repacked copy in Texture.cpp that called vkAllocateMemory directly. It now goes through AllocateImageMemory, and so does the depth stencil copy in DepthSurface.cpp. The host placement log line now carries the cumulative MiB. The debt file records that a placed image stays in host memory for its lifetime. The description is updated: it no longer refers to Context::AllocateDeviceMemory or to #6174 as pending.

All 700 agc_* ctests pass on the RTX 3050 Laptop (615.78.08). On lavapipe, the buffer pool, fallback, graphics and covered-centroid tests pass, and tessellation and depth-storage-reuse are skipped.

Beast of Reincarnation ran 900 s on the previous head 8bb3e07 with no stop and no fault, at the same frame times as main. The driver refused no allocation there, so those runs show the rebase costs nothing; the numbers are in the description.

@adriadam10, the four points from your review should be covered now; could you take another look?

AI-assisted: yes

@GustavoGamarra95

GustavoGamarra95 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@adriadam10 when you have a moment, could you start the full build for this head (9f98c7c)? Only collaborators can run it:

gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=5987

Locally all 700 agc_* ctests pass on the RTX 3050 Laptop. I have not built it on Windows.

Thanks again to @SleglJan, @parl0v, @CnRJay and @ee6aba54 for the runs, data and reviews that shaped this PR.

GustavoGamarra95 and others added 6 commits October 18, 2026 19:29
…a device-local allocation

The budget check in DeviceMemoryType trusts VK_EXT_memory_budget, which lags
behind the driver: with the device-local heap nearly full the budget can still
report room, the allocation fails with VK_ERROR_OUT_OF_DEVICE_MEMORY, and the
title stops. Device buffers (after the pool's reclaim and retry), textures and
storage textures now retry in a host memory type allowed by their requirements.
…refused device memory

Depth targets, depth plane copies, multisampled and render targets, block and
depth copies and attachment proxies allocated device-local memory without the
fallback, so a full device-local heap still stopped the title at them.
…t memory

Render, depth and multisampled targets and attachment proxies that the
driver refuses device memory now go through the buffer pool's recovery:
the pool releases its idle allocations and the attachment is retried in
device-local memory. Only a second refusal places it in host memory, and
that placement is logged with the bytes reclaimed.
… memory

Textures, storage textures, block, depth and depth plane copies and
attachments now share one allocation path: the pool trims its idle tiers,
releases the texture caches, retries device-local memory and places the
image in host memory only when that retry is refused too. Block and depth
plane copies no longer fall back on their first refusal.
…fused reclaim

Repacked copies and depth stencil copies allocated device-local memory
directly and stopped the title on a refusal; they now take the same
allocation path as the other images. Device buffers choose their host
fallback type the same way, the host placement line carries the cumulative
MiB, and the debt file records that a placed image stays in host memory
for its lifetime.
@adriadam10
adriadam10 force-pushed the fix/agc-device-memory-host-fallback branch from 9f98c7c to b8692a7 Compare October 18, 2026 18:00
@adriadam10
adriadam10 merged commit 05171bc into boykopovar:main Oct 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants