Skip to content

perf(agc): grow the resource cache while the keys it evicts come back - #7189

Merged
oneandonlydean merged 3 commits into
boykopovar:mainfrom
tibzejoker:perf/grow-resource-cache
Oct 18, 2026
Merged

oneandonlydean merged 3 commits into
boykopovar:mainfrom
tibzejoker:perf/grow-resource-cache

Conversation

@tibzejoker

@tibzejoker tibzejoker commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

What

The resource cache (ResourceCache, the built ShaderResources that a repeated dispatch or draw binds again) has a fixed bound of 1024 entries. A title that binds more distinct resource sets per frame than that evicts each set before the next frame needs it again, so every frame rebuilds every set.

The bound now grows when that happens:

  • the cache remembers the keys it evicted (as many as its bound);
  • a lookup that misses on a remembered key counts as a return;
  • after each quarter of the bound in evictions, if more than half of those evicted keys came back, the bound doubles, up to 16384 entries. A [rescache] capacity A -> B entries line is printed each time.

The bound also comes back down. Once grown, the cache counts the distinct keys over a window of bound × shrink window uses. When at most a quarter of the bound served that window, the bound halves, down to 1024 at least, and the extra entries leave at the next insert along with the other evictions. The shrink window starts at 8 and doubles, up to 1024, each time growth undoes the last shrink, so the bound stops swinging when keys come back over a longer span. Clear() resets the bound to 1024.

APS5_RESOURCE_CACHE_ENTRIES still sets a fixed bound, with no growth. The table-element budget (APS5_RESOURCE_CACHE_TABLE_ELEMENTS) is unchanged, and so are the evictions it makes.

In GTA V (PPSA04264) in the open world, about 2,400 draws per frame bind about 9,700 distinct sets. On main, the draw path missed the cache 47,236 times per 10 s; the misses repeat keys the cache had evicted ([rescache] miss churn: "same key" 98,736). The draw committer spent about 10 % of its samples building these sets again (ShaderResources::ShaderResources).

Test: agc_driver_resource_cache_growth (tests/execution/ResourceCacheGrowth.cpp). It sets a base bound of 32 entries and a first shrink window of 2 (ResourceCache::SetBase, so the test stays near 1.2k dispatches) and turns dispatch recipes off (APS5_NO_DISPATCH_RECIPE), so every dispatch goes through the cache. It runs 48 compute dispatches, each with its own buffer, four times in turn, and checks that each dispatch stored its value and that the cache then holds all 48 sets. It then checks that two hot sets bring the bound back down, that the next insert trims the cache to it, that the large set grows it again, that the same hot pass then leaves it there (the backoff), and that Clear() resets it to the base. Without the growth it fails: "the resource cache kept only 32 entries (initial bound 32), so every pass rebuilt every set". Without the shrink, the backoff doubling or the Clear() reset, it fails at the matching check.

Related: #6972 (index the resource cache by a four-lane key hash) rewrites the same class's private members for a different purpose, so the two conflict textually. Whichever merges second needs a short rebase; the growth only needs the evicted keys' hashes, which #6972 already stores in each entry. #6492 (drop entries that hold released sampled textures) adds a member next to the same lines; only those lines need merging by hand.

Tested

Windows 11 Pro (26200), RTX 4090 (NVIDIA 616.92), i9-13900K, MinGW GCC 15.2.0:

[rescache] draws misses / 10 s committer µs per draw commits / 10 s
base 47,236 41.8 202k
this PR 120 38.2 219k

The bound grew 1024 -> 2048 -> 4096 -> 8192 -> 16384, and the cache settled at 9,708 entries. Process RAM was the same in both runs (25 GB).

  • Performance trace summaries, one run each, APS5_TRACE_SECONDS=240:60, same scene and session, no other APS5_TRACE_*/PROFILE_* variables:
presents in 60 s frame p50 / p90 / p99 GPU busy per present
base (7a90c06ec) 263 216 / 258 / 501 ms 63.6 ms
this PR (8391a52e3) 459 128 / 213 / 330 ms 44.3 ms

Traces: base, this PR. Both commits are the base above plus two local bench hooks that do nothing unless their variable is set. Read these numbers with care: the frame rate in this scene depends on where the save respawns the player, both runs were still compiling some shaders (Driver.Draw.shader_compile_and_link 106 and 55 ms per frame), and queue worker 0's trace buffer filled after about 4 s in both runs (trace_dropped). The committer figures above are the steadier comparison.

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
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA RTX 4090, driver 616.92, Windows 11
  • AI-assisted: yes, AI-assisted (Claude Code)

🤖 Generated with Claude Code

https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P

@github-actions

github-actions Bot commented Oct 17, 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 6972 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp L661-668, L671-672
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L3304-3305, L3309-3310, L3438-3440, L3445-3447, L3491-3493

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 5998 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6054 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6082 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6177 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6194 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6223 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6340 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6373 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6412 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6414 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6481 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6492 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6498 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6636 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6647 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6783 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6804 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6973 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6987 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6989 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6995 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7002 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7154 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7258 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp

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

Copy link
Copy Markdown
Contributor

AMD data point, since the description lists NVIDIA: head c652880f4 as-is, Release, Windows 10 Pro 22H2 (19045), WinLibs GCC 15.2.0 r7, AMD Radeon RDNA 2 iGPU (1002:164e, driver 32.0.21045.5002) chosen with ANYPS5_GPU.

  • agc_driver_resource_cache_growth: 5 of 5 pass (not skipped).
  • Negative control: only the runtime sources reverted to the merge-base (test kept) fails with 1536 dispatches with distinct resources repeated 4 times in turn, but the resource cache kept only 1024 entries (initial bound 1024).

Not checked: other tests, titles, timings.

AI-assisted: yes (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this. Review and full CI pass on c652880, and our games gate shows no regressions, but the cache growth needs one change before we merge.

In Astro Bot the cache grows from 1024 to 8192 entries, and peak VRAM goes from 13.2-13.7 GB on main to 14.6 GB with this PR (about +1.0-1.4 GB). The bound only ever grows, not even Clear() resets it, so one heavy scene keeps the larger cache for the rest of the process. Neither of our gate titles shows an FPS gain, so on those titles it's extra VRAM for nothing.

Could you let the bound come back down? For example, reset it to the base size on Clear() and shrink it again when the evicted keys stop coming back, or cap the growth lower than 8192. Either way, please include a test that the bound shrinks.

If you can also post the miss/return counters from GTA V before and after (the 47k -> 120 per 10 s numbers), with the VRAM figure from the same run, that would help us weigh the trade-off.

@tibzejoker

Copy link
Copy Markdown
Contributor Author

Thanks for the Astro Bot numbers, that's a fair point. I'll let the bound come back down: reset to the base size on Clear(), and shrink it once evicted keys stop coming back. I'll add a test that the bound shrinks, and post the GTA V miss/return counters with peak VRAM from the same run, before and after.

AI-assisted (Claude Code).

@oneandonlydean oneandonlydean self-assigned this Oct 17, 2026
Halve the grown bound (not below 1024 entries) once a window of the
bound's uses times a shrink window was served by at most a quarter of
it, and reset the bound on Clear(). The shrink window starts at 8 and
doubles (up to 1024) each time growth undoes the last shrink, so a set
of keys that comes back over a longer span stops the bound from
oscillating. The entries over the bound leave at the next insert, with
the other evictions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
@tibzejoker

Copy link
Copy Markdown
Contributor Author

Done in bea4be8: the bound now comes back down.

  • Shrink. Once the bound has grown, the cache counts the distinct keys over a window of bound × shrink window uses (Find hits, Touch hits, inserts). If at most a quarter of the bound served that window, the bound halves, but never below 1024. The entries over the new bound leave at the next insert, along with the other evictions, so a caller under the GPU mutex still hands them to the recorder. I didn't trim inside Find: it can run without the GPU mutex (PrepareDispatch), and freeing a set returns it to descriptorCache.
  • Backoff. The shrink window starts at 8 and doubles, up to 1024, each time growth undoes the last shrink. Without it, GTA swung between 4096 and 8192 about ten times in 5 minutes: a 2.7 s window saw only about 1.5k keys, but the evicted keys came back later.
  • Clear() resets the bound to 1024, and the shrink window to 8.
  • The window is only tracked while the bound is above 1024, so a cache at the default size has no extra work.

Test (agc_driver_resource_cache_growth, 1536 dispatches with distinct sets, then 64 of them repeated 640 times):

  1. The bound grows past 1024.
  2. The hot pass brings it down.
  3. The next insert trims the entries to the new bound.
  4. The large set grows it again.
  5. The same hot pass then leaves it in place (backoff).
  6. Clear() resets it to 1024.
  7. The large set grows it once more.

Run on c652880, the test fails at step 2 ("64 sets served 40960 dispatches, but the bound stayed at 2048 entries"). Without the doubling, it fails at step 5. ctest -R "agc_driver|agc_masked|agc_shader": 524/524 pass, 2 skipped, RTX 4090.

GTA V, open world. Build: main + #6488 + #6848, same save, one run each, last 12 windows of 10 s. VRAM is the whole GPU's memory.used from nvidia-smi, sampled every 2 s over the run; the idle desktop accounts for about 1.7 GB of it.

[rescache] draws misses / 10 s bound at the end peak VRAM median VRAM committer µs per draw commits / 10 s
base 25,047 1024 7.69 GB 7.29 GB 40.3 209k
#7189, shrink without backoff 437 4096 ↔ 8192 7.66 GB 7.53 GB 34.1 239k
#7189 now (bea4be8) 114 8192 7.52 GB 7.06 GB 38.3 217k

The base misses are lower than the 47k in the PR body because the respawn point changes the scene, and misses vary a lot between runs. The committer figures carry the same noise. With the backoff, GTA settles after two shrinks, at 8192 entries (it used to reach about 9.7k at the 16384 cap). In this title the larger cache costs no measurable VRAM: the peak is within run-to-run noise of main.

AI-assisted (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for adding the shrink. The bound now comes back down as asked, and the review of bea4be8 found no problems in the cache code.

One thing blocks CI: the new agc_driver_resource_cache_growth test times out on the Linux runner (llvmpipe). In full CI run 39081985777 on bea4be8 it was killed at the 120 s ctest limit, and it was the only failure (Linux 1 of 1406; it skips on Windows, which has no Vulkan device). On the earlier head c652880 it passed but already took 48.8 s, and CONTRIBUTING.md asks for tests to finish within 30 s. The extra shrink and regrow passes push it to roughly 100k dispatches.

Could you make the test cheaper rather than raising the timeout? One option is a test-only way to set a smaller base bound and window, so the grow, shrink and regrow steps take a few thousand dispatches instead of ~100k. It should still fail without the doubling and without the Clear() reset, as it does now. We'll rerun full CI on the new head.

The test now sets a base bound of 32 entries and a first shrink window
of 2 (ResourceCache::SetBase, applied at the next Clear()) and turns
dispatch recipes off, so every dispatch goes through the cache: grow,
shrink, regrow, backoff and the Clear() reset take about 1.2k
dispatches instead of about 100k.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
@tibzejoker

tibzejoker commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. f27ad68 makes the test short without touching the timeout.

  • ResourceCache::SetBase(entries, shrinkWindow) sets the bound a cleared cache starts from and shrinks back to (default 1024) and the first shrink window (default 8). It takes effect at the next Clear(). The test sets 32 and 2, so its working sets are 48 sets and 2 hot sets.
  • The test also sets APS5_NO_DISPATCH_RECIPE=1, the way ImageTableCacheBounds sets its budgets. With the small sizes, a dispatch recipe hit only Touches its template and never re-inserts it, so after an eviction or a Clear() some sets bypassed the cache and the bound saw no misses. With recipes off, every dispatch goes through Find/Insert.

Every step runs in about 1.2k dispatches in total (it was about 100k): grow, shrink, trim at the next insert, regrow, the backoff, then Clear() and grow again. Here it takes 0.5 s on the RTX 4090. I have no llvmpipe setup, so the CI run will show the Linux time.

Each control still fails at its own check:

  • Without the growth: "48 dispatches with distinct resources repeated 4 times in turn, but the resource cache kept only 32 entries (initial bound 32)".
  • Without the shrink: "2 sets served 320 dispatches, but the bound stayed at 64 entries".
  • Without the backoff doubling: "the bound came down again after the same hot pass, although the last shrink had been undone".
  • Without the Clear() reset: "Clear() left the bound at 64 entries instead of 32".

ctest -R "agc_driver|agc_masked|agc_shader": 524/524 pass, 2 skipped. Conventions pass. The PR body is updated.

AI-assisted (Claude Code).

@oneandonlydean
oneandonlydean merged commit 5dd2e36 into boykopovar:main Oct 18, 2026
10 checks passed
tibzejoker added a commit to tibzejoker/AnyPS5 that referenced this pull request Oct 21, 2026
The graphics pipeline store kept a fixed bound of 1024 entries, and the
journal replayed only its first 1024 records, which the replay then
skipped once the store was full. A title with more pipelines rebuilt
the rest at the draw under the GPU mutex, about 200 ms each. In GTA V's
open world that is 80 to 150 rebuilds in about six minutes of play;
with this change, 42 (pipelines no earlier run recorded).

- The journal replays up to 16384 records (APS5_PIPELINE_JOURNAL_ENTRIES
  still sets it), and a replayed pipeline that finds the store full
  doubles the bound instead of being skipped, up to 16384.
- During play, the store keeps the hashes of the pipelines it evicted,
  as the resource cache does (boykopovar#7189). Every bound/32 evictions, if more
  than half came back, the bound doubles, up to 16384.
- The bound never shrinks during a run: a pipeline is cheap to keep and
  costly to rebuild. A device that clears its pipelines resets it, and
  APS5_PIPELINE_STORE_ENTRIES still fixes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017EjjFU1FjMR11oWDYs8UMo
tibzejoker added a commit to tibzejoker/AnyPS5 that referenced this pull request Oct 22, 2026
The graphics pipeline store kept a fixed bound of 1024 entries, and the
journal replayed only its first 1024 records, which the replay then
skipped once the store was full. A title with more pipelines rebuilt
the rest at the draw under the GPU mutex, about 200 ms each. In GTA V's
open world that is 80 to 150 rebuilds in about six minutes of play;
with this change, 42 (pipelines no earlier run recorded).

- The journal replays up to 16384 records (APS5_PIPELINE_JOURNAL_ENTRIES
  still sets it), and a replayed pipeline that finds the store full
  doubles the bound instead of being skipped, up to 16384.
- During play, the store keeps the hashes of the pipelines it evicted,
  as the resource cache does (boykopovar#7189). Every bound/32 evictions, if more
  than half came back, the bound doubles, up to 16384.
- The bound never shrinks during a run: a pipeline is cheap to keep and
  costly to rebuild. A device that clears its pipelines resets it, and
  APS5_PIPELINE_STORE_ENTRIES still fixes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017EjjFU1FjMR11oWDYs8UMo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants