Skip to content

perf(agc): keep released image memory in the buffer pool for reuse by type and size - #7870

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
ionut27001:perf/image-memory-pool
Oct 20, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
ionut27001:perf/image-memory-pool

Conversation

@ionut27001

@ionut27001 ionut27001 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

The buffer pool keeps released device buffers for reuse, but an image's own VkDeviceMemory is allocated with vkAllocateMemory and freed with vkFreeMemory one image at a time. In a scene that re-creates its storage images at every dynamic-resolution step (Marvel's Wolverine's "Sony Interactive Entertainment presents" card, the churn of #6099), the dispatching queue worker spends most of its image-table resolution allocating and releasing images: of the 4129 samples inside ShaderResources::resolveImageTables in a 300 s APS5_SAMPLE_WORKER capture of main (with APS5_PROFILE_DRAW=1, whose prints took 98 of them), 1960 (47 %) are in StorageTexture::release (vkFreeMemory, vkDestroyImage and the views' destruction, which the stacks do not tell apart) and 1634 (40 %) in StorageTexture::Create (936 of them in BufferPool::AllocateMemory); 2888 (70 %) end in win32u.dll, the kernel calls behind the NVIDIA driver's allocation and release (collapsed stacks in the evidence zip, sampled-main/).

  • BufferPool::PutImageMemory keeps the device memory of a released image in the device tier, keyed by memory type and exact size, under its own budget: APS5_IMAGE_POOL_MIB, a quarter of the device tier's budget by default (0, or APS5_STAGING_POOL_MIB=0, frees image memory at once, as main does). It retains nothing while any device-local heap's VK_EXT_memory_budget usage is over its budget (read at most every 100 ms; never without the extension) and then also frees the image memory it holds, while retained blocks are still handed out meanwhile; host-placed allocations, allocations of unknown size and allocations larger than the image budget are freed at once. Retained image memory counts in the device tier's budget and slot bound and is evicted least recently used together with the tier's buffers, so a burst of image releases can also push retained staging buffers out.
  • BufferPool::TakeImageMemory hands a retained allocation of the same type and size to the next image before allocating; AllocateImageMemory goes through it and reports the type it allocated.
  • The out-of-memory recovery of AllocateMemory trims the pool again (every tier and the spare slab blocks) after a cache release, since the images that release destroys now return their memory to the pool, and it also runs for VK_ERROR_TOO_MANY_OBJECTS, since retained allocations hold allocation handles.
  • StorageTexture and the sampled textures' OwnedImage record their allocation's size and type and release it through ReleaseImageMemory. Depth surfaces, multisampled targets, render targets and the attachment proxies still allocate through AllocateImageMemory, so they take a retained block of a matching type and size when there is one, but keep freeing their memory directly.
  • The [bufferpool] profile line gains the image counts (hits, misses, evictions, retained).

Which images are created, when they are evicted and what they hold do not change: a retained allocation is bound to a new image that starts in VK_IMAGE_LAYOUT_UNDEFINED and is uploaded as before. An image is released only once no recorded batch keeps it, which is what already let main free its memory there; a keep that was missing would have been masked on main by the driver deferring an in-use free, while with the pool the block is bound to the next image of that size at once (APS5_NO_KEEP_NEW_STORAGE is such a case, noted in its row of docs/dev/ENVIRONMENT.md).

Tested

Windows 11 Pro 26300, Ryzen 7 5800X3D, 32 GB RAM, RTX 4080 SUPER 16 GB (NVIDIA 617.42), WinLibs GCC 15.2.0 r7, Release, BUILD_TESTING=ON.

{"commit":"c024ba034b49","dirty":false,"env":{"APS5_KEPT_DESCRIPTOR_SETS":"0","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"10:12"},"gpu":"NVIDIA GeForce RTX 4080 SUPER","driver":"NVIDIA 617.42","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 10:12","first_present":10,"ms":76850.693},"presents":12,"frame_ms":{"p50":6009.057,"p90":8511.643,"p99":8604.445,"max":8604.445},"gpu_busy_ms_per_present":1106.740,"worker_busy_pct":{"queue worker 0x0":99.923,"queue worker 0x28":0.038,"queue worker 0x20":98.760},"top_phases_ms_per_frame":{"Driver.Packet":12681.928,"Driver.Packet.dispatch":6325.430,"Driver.Dispatch":6324.631,"Driver.Packet.memory_wait":6274.826,"Vulkan.Dispatch":5490.262,"Vulkan.Dispatch.shader_resources":5488.643,"Resources.Complete":5488.297,"Driver.EndSubmission":44.524},"trace_dropped":false,"trace":"aps5-trace-c024ba034b49-41080.json.gz"}
{"commit":"c024ba034b49","dirty":false,"env":{"APS5_KEPT_DESCRIPTOR_SETS":"0","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"10:12"},"gpu":"NVIDIA GeForce RTX 4080 SUPER","driver":"NVIDIA 617.42","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 10:12","first_present":10,"ms":67608.415},"presents":12,"frame_ms":{"p50":5667.689,"p90":5858.166,"p99":6023.004,"max":6023.004},"gpu_busy_ms_per_present":1058.698,"worker_busy_pct":{"queue worker 0x0":99.955,"queue worker 0x20":98.671,"queue worker 0x28":0.137},"top_phases_ms_per_frame":{"Driver.Packet":11138.148,"Driver.Packet.dispatch":5554.701,"Driver.Dispatch":5554.102,"Driver.Packet.memory_wait":5504.770,"Vulkan.Dispatch":4817.111,"Vulkan.Dispatch.shader_resources":4815.729,"Resources.Complete":4815.452,"Driver.EndSubmission":60.152},"trace_dropped":false,"trace":"aps5-trace-c024ba034b49-40472.json.gz"}
{"commit":"58d293532c03","dirty":false,"env":{"APS5_KEPT_DESCRIPTOR_SETS":"0","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"10:12"},"gpu":"NVIDIA GeForce RTX 4080 SUPER","driver":"NVIDIA 617.42","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 10:12","first_present":10,"ms":56301.046},"presents":12,"frame_ms":{"p50":4683.760,"p90":4742.898,"p99":4891.576,"max":4891.576},"gpu_busy_ms_per_present":989.094,"worker_busy_pct":{"queue worker 0x0":99.584,"queue worker 0x20":98.537,"queue worker 0x28":0.055},"top_phases_ms_per_frame":{"Driver.Packet":9252.764,"Driver.Packet.dispatch":4600.420,"Driver.Dispatch":4599.650,"Driver.Packet.memory_wait":4575.680,"Vulkan.Dispatch":3994.348,"Vulkan.Dispatch.shader_resources":3992.903,"Resources.Complete":3992.595,"Driver.EndSubmission":45.090},"trace_dropped":false,"trace":"aps5-trace-58d293532c03-9900.json.gz"}
ms main (1st) main (2nd) this PR
frame p50 6009 5668 4684
frame p90 8512 5858 4743
frame max 8604 6023 4892
12 presents 76851 67608 56301
GPU busy per present 1107 1059 989
Resources.Complete per frame 5488 4815 3993
Driver.Packet.memory_wait per frame 6275 5505 4576

Against the second main capture: frame p50 −17 %, the 12 presents −17 %, Resources.Complete per frame −17 %; against the first: −22 %, −27 %, −27 %. The two main captures differ between themselves by 6 % at p50 and 12 % over the 12 presents, so the gain is 17–27 % on these figures here, not a sharper number. An earlier capture of this change (commit 62f3819a2548, before the review fixes: the over-budget rule, the recovery trim, VK_ERROR_TOO_MANY_OBJECTS and the device-budget bound on what is retained) gave p50 4991 ms, p90 7467 ms and 67992 ms for the 12 presents; it is attached as well. tools/trace_digest.py output of every capture and tools/performance_compare.py of main (1st) against this PR are in the evidence zip. Attachments: image-memory-pool-traces.zip holds the four aps5-trace-*.json.gz captures unchanged (GitHub refuses a .json.gz attachment on its own), image-memory-pool-evidence.zip the summary lines, digests, sampled stacks, test logs and pool counters.

In a separate 200 s run of this PR with APS5_PROFILE_DRAW=1 (profile-pool2/ in the evidence zip), the last [bufferpool] line (the 19th, printed at a buffer take at least 190 s in) reads images: 68989 hits, 221690 misses, 219556 evictions, 0 retained (0 MiB of 477): the 477 MiB image budget holds about a tenth of a second of this scene's churn (the run's last cumulative [texture] line counts 293300 storage images of 1153687 MiB in 200 s, about 5.6 GiB/s), so a quarter of the re-creations take their memory from the pool and the rest allocate; 13 of the 19 lines show 0 retained, the others 68 to 303 images. A larger APS5_IMAGE_POOL_MIB would raise the hit rate at the price of idle device memory; I kept the quarter.

  • The focused AGC tests (agc_buffer_pool, agc_buffer_pool_small_device_shared, agc_buffer_pool_small_device_no_staging, agc_storage_upload_lifetime, agc_storage_write_back, agc_driver_graphics, agc_driver_image_table_reuse, agc_driver_storage_cache_budget) on main f78675a + feat(libs): restore libSceNpCppWebApi for dumps that ship no copy #6901 and on this PR, with ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1 and VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation set in the shell that ran ctest -V (the logs list only the variables CMake sets): all pass on both (focused-tests/ in the evidence zip). The Khronos validation layer is not installed on this PC, so neither log holds VUID- lines and the comparison of validation errors that CONTRIBUTING asks for is still owed; I will add it once the layer is installed.

Not tested: Linux; other GPUs; an integrated GPU (where the device-local types are usually host-visible too, so DeviceTiered is false and nothing is retained; one that exposes a device-local-only type retains as a discrete card does); a title whose image sizes do not repeat (every release then retains and evicts another: the frees are delayed and up to the image budget of device memory idles in the pool).

Overlaps: #6481 changes the line of the sampled texture's constructor that this PR changes (Texture.cpp L350 on main, here owned->Allocate(...)). #4472 changes the device tier's retention, #7119 AllocateMemory's failure diagnostics and the slot bound, and #7800 AllocateMemory's signature and the cache release it calls; they conflict with main today, so tools/pr_overlap.py could not compare them, and whichever lands second merges by hand. The report lists about thirty more open PRs that share Texture.cpp, Texture.hpp or docs/dev/ENVIRONMENT.md (and, for #6292 and #6615, BufferPool.hpp, whose mutex type they change) and conflict with main; those are textual. Retained image entries sit in the device tier, so #4472's pressure policy would cover them only once it is made to see PutImageMemory.

Checklist

  • Based on current main; no other open PR implements the same functions

  • One topic per PR; follow-ups go in a new PR

  • No comments in code except technical debt

  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file

  • No notes, investigation .md files or images added to the repository (attach them to this PR)

  • New third-party code is a submodule built from source

  • Generic behaviour, not specific to one title
    Rebase: the branch was rebased onto main 1e774a3 and now has a second commit, bafc97bb1e, which answers the reviews' O(1) request. Merging main duplicated the physical, memoryProperties and memoryProperties2 members (main added them for the failure diagnostics, this PR for the heap budget), so the rebased commit keeps one declaration of each, and the profile commit 58d293532 is no longer patch-identical to the PR's first commit. Re-tested on the new base: full build with WinLibs GCC 15.2.0 (Windows 11), exit 0; agc_buffer_pool, agc_buffer_pool_small_device_shared and agc_buffer_pool_small_device_no_staging passed on two consecutive runs (2.88 s / 0.41 s for the first). With only bafc97bb1e reverted the same three tests still pass, as a behaviour-preserving refactor should; with APS5_IMAGE_POOL_MIB=0 the agc_buffer_pool test still fails at the out-of-memory step, as described above.

  • Depends on: none

  • Fixes: none

  • Unblocks: none

  • GPUs tested: NVIDIA GeForce RTX 4080 SUPER, driver 617.42, Windows 11 Pro 26300

  • AI-assisted: yes

@github-actions github-actions Bot added the perf Performance change label Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 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 8672 core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp L330

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/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1395 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 4472 core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp
core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp
PR 5431 docs/dev/ENVIRONMENT.md
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6187 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6209 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6280 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6481 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6636 docs/dev/ENVIRONMENT.md
PR 6682 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6683 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6879 docs/dev/ENVIRONMENT.md
PR 6924 docs/dev/ENVIRONMENT.md
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
PR 6960 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7002 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7048 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7085 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7449 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
PR 7464 docs/dev/ENVIRONMENT.md
PR 7484 docs/dev/ENVIRONMENT.md
PR 7514 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7604 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
PR 7800 core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp
PR 7922 docs/dev/ENVIRONMENT.md
PR 8004 docs/dev/ENVIRONMENT.md
PR 8077 docs/dev/ENVIRONMENT.md
PR 8206 docs/dev/ENVIRONMENT.md

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

Verified on Windows 11 with MinGW-w64 GCC 15.2.0 (Release):

  • Architecture & Implementation: Caching released image VkDeviceMemory within BufferPool by type index and size under APS5_IMAGE_POOL_MIB directly addresses allocations churn during dynamic resolution changes without introducing device memory unbounded growth.
  • Conventions & Docs: Environment variable APS5_IMAGE_POOL_MIB documented in docs/dev/ENVIRONMENT.md. Zero code comments added; tools/check_conventions.py passes with 0 findings.
  • Unit Testing: Built agc_buffer_pool_tests.exe and verified ctest --test-dir build-libs -R "^agc_buffer_pool$" -V passes in 0.21s:
    1/1 Test #288: agc_buffer_pool ..................   Passed    0.21 sec
    
    All recovery and slab validation fixtures passed.

@github-actions github-actions Bot added docs agc libSceAgc* labels Oct 19, 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.

The profile is convincing (47% of resolveImageTables samples in StorageTexture::release, 40% in Create, 70% ending in kernel calls) and the budget rules (nothing retained over budget, nothing for host-placed or unknown-size allocations) are thought through. The gain is real but smaller than the headline, and one hazard needs a test.

  • The A/B is 12 presents per capture on a scene running at 5 to 8 s per frame. The two main captures differ by 6% at p50 and 12% over the 12 presents; the gain is 17 to 27%. That is outside the spread but not by much, and 12 frames of one loading card is the best case for this pool (an identical image size repeating at every resolution step). The same run's own [bufferpool] line shows 68,989 hits against 221,690 misses and 0 retained in 13 of 19 lines, so 24% of re-creations reuse memory. Please add a pair in a scene that is not the churn card (gameplay, same title), to show the cost where sizes do not repeat. The description names that case as untested.
  • The hazard the description states: a block is bound to the next image of that size at once, while on main a driver that defers an in-use free hid a missing keep (APS5_NO_KEEP_NEW_STORAGE is one). So every missing keep in the tree, today invisible, becomes a use-after-reuse that corrupts a texture. The Khronos validation comparison is "still owed". Please run it with synchronization validation on the focused tests and on a Wolverine capture before this lands, since the validation layer is the only thing that finds this class.
  • Each release scans the device tier's keys for the oldest image, on the queue worker, at a measured 219,556 evictions in 200 s. That is a linear scan per release on the path the PR is speeding up. Use an LRU list or an ordered index so a release and an eviction are O(1).
  • The pool holds up to a quarter of the device tier's budget as idle device memory. For a title with a heap already near its budget (#4472, #5987) the pool should give it back first. State what happens when AllocateMemory fails: does the recovery trim free the pooled image memory before it reports the failure (the description says it does) and is that tested with a full pool?
  • Only Windows and an RTX 4080 SUPER. An AMD or Linux run of agc_buffer_pool* and the same trace pair would show the behavior is not specific to the NVIDIA driver's slow vkFreeMemory.

@adriadam10 adriadam10 self-assigned this Oct 19, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Read the diff; the accounting (imageSlots/imageBytes in take, put, both evictions and trim), the over-budget rule and the OOM trim are consistent, and the new tests pin allocation/free counts. What is still missing before this can land, in line with @CnRJay's review:

  • Validation comparison (CONTRIBUTING): the VUID set with and without the change is still owed. It matters more here than usual: a retained block is bound to the next image of that size at once, so any missing keep that main hides behind a deferred driver free becomes a texture overwrite. Please run the focused tests and a Wolverine capture with the Khronos layer (synchronization validation on) and post the VUID diff.
  • BufferPool.cpp:414-418 (evictOldestImage) scans every key of the device tier on each eviction and over-budget release, on the queue worker, at ~220k evictions per 200 s. Keep the images in an ordered index or LRU list so release and eviction are O(1).
  • A pair in a scene that is not the churn card (gameplay of the same title), where sizes do not repeat, to show the cost of delayed frees and idle pool memory. The 12-present card is the best case and main varies 6-12 % between its own captures.
  • Peak device memory (budget usage) and RSS before/after on that workload, since up to a quarter of the device tier idles in the pool.
  • If you can, one run of agc_buffer_pool* and the trace pair on AMD or Linux, to show the gain is not specific to the NVIDIA vkFreeMemory cost.

… type and size

An image's own VkDeviceMemory was allocated and freed one at a time, and in a scene that re-creates its storage images at every dynamic-resolution step the dispatching queue worker spent half of its image-table resolution freeing evicted images and most of the rest allocating new ones. The pool now keeps the device memory of a released storage or sampled image under its own budget (APS5_IMAGE_POOL_MIB, a quarter of the device tier's budget by default) and hands it to the next image of the same memory type and size.
A release scanned every key of the device tier for the oldest retained
image, on the queue worker, once per retained image the budget does not
hold. Retained images are now also kept in one list in release order,
and each retained slot holds its own node, so a release picks and
evicts the oldest image in constant time and a take removes its node in
constant time.

The device-tier eviction that a release can also trigger when the
buffer budget or the slot count is reached still scans the tier's keys;
that path predates this change and is untouched.
@ionut27001
ionut27001 force-pushed the perf/image-memory-pool branch from 99ef6fa to bafc97b Compare October 19, 2026 20:42
@ionut27001

Copy link
Copy Markdown
Contributor Author

bafc97bb1e makes the image eviction O(1). Retained images are also kept in one list in release order, and each retained slot holds its node, so a release evicts the oldest image without scanning the device tier's keys, and a take removes its node, in constant time. What is retained, evicted or reused does not change. The device-tier eviction that a release can also trigger when the buffer budget or the slot count is reached still scans the tier's keys; that path predates this PR and is untouched.

The out-of-memory trim with a full pool is covered by ImageMemoryReuse: 32 four-MiB images fill the image budget, then the next allocation fails once, the trim releases all 32 of them (RetainedImages() 0, the free count goes from 3 to 35) and the retry succeeds. ImageMemoryReclaim covers the same trim reached from a cache release.

The branch was rebased onto main 1e774a3. On Windows 11 with WinLibs GCC 15.2.0 it builds, and agc_buffer_pool, agc_buffer_pool_small_device_shared and agc_buffer_pool_small_device_no_staging pass on two consecutive runs; reverting only bafc97bb1e keeps them passing, as a refactor that changes no behaviour should, and APS5_IMAGE_POOL_MIB=0 still fails the pool test at the out-of-memory step.

Not done here: the Khronos validation comparison, the capture pair in a scene whose image sizes do not repeat, peak device memory and RSS on that workload, and a run on AMD or Linux.

@ionut27001

Copy link
Copy Markdown
Contributor Author

Validation and memory evidence for the remaining review items.

Setup. Windows 11 Insider build 26300 (reported as 10.0.26300), Ryzen 7 5800X3D, 32 GB, RTX 4080 SUPER 16 GB, driver 617.42, WinLibs GCC 15.2.0, Khronos validation layer 1.4.363 with synchronization validation requested (the layer's VALIDATION-SETTINGS notice appears once per instance in every log). Main is 1e774a3b22, the PR's base (origin/main has since moved to 9127ea36d2); the PR is bafc97bb1e. The relinked wolverine.exe is the same file in every run.

Focused tests. agc_buffer_pool*, agc_driver_image_table*, agc_driver_runtime_images, agc_import_settle_storage_image and agc_placeholder_images, 12 tests with ANYPS5_REQUIRE_VULKAN=1: 12/12 pass on both. The VUID set is identical, one message of VUID-vkCmdDispatch-magFilter-04553 on each.

Wolverine, splash card, no input, 900 s cap per arm: identical VUID sets, VUID-VkImageViewCreateInfo-usage-02275 (at least 10 messages; the layer stops at 10 duplicates) and VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (2 messages) on both, and no SYNC-* message on either. The change adds no VUID here.

Memory, four 300 s runs alternating main/PR/main/PR, mean of the per-run peaks over two runs per arm: working set 14,717 vs 14,696 MiB, private bytes 27,951 vs 27,932 MiB, whole-device VRAM from nvidia-smi (sampled every 2 s) 13,318 vs 12,830 MiB. On the splash card the image pool logged about 131k misses against 44k hits and 129k evictions in 300 s and held nothing at the end; this is still the churn card, not the non-repeating scene that was asked for.

Not done: AMD or Linux; a scene after the splash card (the title presented 2 to 32 frames in 300 to 900 s with no input, so none was reached); a VUID set scoped to an APS5_TRACE_FRAMES=10:12 window, since no trace window was enabled during the validation runs.

@adriadam10
adriadam10 merged commit be1ce98 into boykopovar:main Oct 20, 2026
12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Re-checked against the earlier points: evictOldestImage is now O(1) through the release-order list (node removed on take, cleared on trim, kept consistent by the generic device-tier eviction); the VUID sets with and without the change are identical on the focused tests and on Wolverine with synchronization validation (no SYNC-*); working set and private bytes are unchanged and device VRAM is lower with the pool (12,830 vs 13,318 MiB). The new tests pin allocation/free counts, including the OOM trim and the over-budget release. Not covered: a scene with non-repeating sizes and AMD/Linux, which the queue's Linux run exercises only for the tests; merged.

Celegans12 added a commit to Celegans12/AnyPS5-Ghost-of-Yotei that referenced this pull request Oct 21, 2026
346 upstream merges since b509767. 37 conflicted files / 100 hunks,
resolved per research/structural/merge21 notes (execution, graphics,
recompiler+tests). Versions: FormatVersion 50, RuntimeAbi 18, request
serialization 42 (upstream 39 mergedHull and 41 f32 denorm as written;
our single-workgroup flag is 42). Upstream's image memory pool (boykopovar#7870)
is opt-in with APS5_IMAGE_POOL=1.

AI-assisted: yes
Celegans12 added a commit to Celegans12/AnyPS5-Ghost-of-Yotei that referenced this pull request Oct 21, 2026
- ImageArrayWindowPastLayerLimit (upstream test): MinGW GCC refuses
  alignas(65536) on a static object; the texels now come from an aligned
  heap allocation.
- BufferPool ImageMemoryOverBudget (boykopovar#7870): uses our mock's heap budget
  and usage fields.
- WriteTracking CheckImportWindowOverUnwatchedPart: with boykopovar#8172's per-span
  unwatch the watched blocks stay watched; the unwatch hook must cover the
  part that leaves the watch.

AI-assisted: yes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs perf Performance change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants