Skip to content

perf(agc): let the storage image cache take up to half of the heap budget - #10044

Merged
oneandonlydean merged 4 commits into
boykopovar:mainfrom
JJenkx:perf/agc-storage-cache-half-heap
Oct 24, 2026
Merged

oneandonlydean merged 4 commits into
boykopovar:mainfrom
JJenkx:perf/agc-storage-cache-half-heap

Conversation

@JJenkx

@JJenkx JJenkx commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What

The storage image cache's budget is what the reported heap budget leaves after three things: the heap's use outside the texture caches, the live sampled textures, and an eighth of the budget. That result is then capped at the smaller of TextureCacheBudget (a quarter of the heap, at least 2 GiB) and a quarter of the budget. On an 8 GB GPU, the quarter caps bind first, at about 1.5-1.7 GiB, even when the budget formula leaves far more.

Echoes of the End (PPSA25636) needs about 3.5-4 GiB of storage images in play. With the quarter caps it re-creates them all the time. In gameplay, with the default budgets, it evicted about 2000 storage images (about 22 GiB) every 10 s, and re-created and re-uploaded them on their next use. The technical debt entry for the budgets describes this case: "a title whose storage working set exceeds the shrunk budget re-creates storage images at each use".

This PR changes only the caps, to half of the heap's size and half of the reported budget. The reserve is unchanged: the other use, the live sampled textures and the eighth of the budget are still kept out. The 512 MiB floor and the fallback without VK_EXT_memory_budget are also unchanged. A heap under pressure therefore still shrinks the storage cache as before. The higher cap applies only when the budget has room.

Tested

  • storageTextureBudgetTests is revised on purpose:
    • Expectations that pinned the quarter now expect the half.
    • The pressure sweep checks the new caps (half of the budget above the floor, half of the heap).
    • The sweep keeps its other invariants: the floor, monotonic shrinking as non-cache use grows, and no live sampled memory or eighth of headroom taken.
  • Current head 9904c737ca (adds the sampled-budget storage reservation from review). Exact-head gate: main 1b5efcc8d9 16/16; this PR with the new reservation reverted 15/16, failing only "a storage image cache larger than TextureCacheBudget was not reserved in full"; this PR 16/16. 4 layer insertions and 0 VUIDs per arm. Details: perf(agc): let the storage image cache take up to half of the heap budget #10044 (comment). The joint sweep asserts that above the floors neither cache budget plus the other cache's held bytes exceeds the reported budget less its eighth.
  • Earlier focused Vulkan gate (head before the review fix), same build directory and command for every arm. Command: ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer ctest -V -j1 -R '^(agc_driver_graphics|agc_sparse_.*|agc_heap_mirror_.*|agc_host_import.*)$'. Machine: Linux 7.2.9, RX 6650 XT, RADV (Mesa 26.2.4).
    • Results:

      arm head result validation layer inserted VUIDs
      main b7a451d62b 15/15 4 tests 0
      main + this PR's test file only b7a451d62b + tests/Graphics.cpp from the change 14/15: agc_driver_graphics fails with "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to half of the budget" 4 tests 0
      this PR 92b3bf8f8b (the same source and tests as the head a2f89cad59, whose later commit only condenses the debt entry) 15/15 4 tests 0

      Every arm prints only the two existing Windows guest-arena skips ("heap mirrors not tested", "heap mirrors of imported ranges not tested").

  • Echoes of the End (PPSA25636), same machine. The title needs open PRs to reach play (fix(shader): preserve read lane dominance across branches #8089, feat(libSceAgcDriver): import scattered direct memory as a dma-buf run list and probe its coherence #4449, fix(agc): lower import budgets only on memory exhaustion #8091, fix(agc): keep the write-back ready barrier while hole copies are pending #8908, perf(agc): mirror a sparse heap whose committed span crosses adjacent ranges #9551, perf(agc): join the imports of a buffer bound from the start of adjacent ranges #9652). The arms are the same integration tree without this change (e3f17c7043) and with it (52893755e9), in one build directory. Both carry private diagnostic commits; none of them is part of this PR.
    • Each arm starts from the same seed (the save in play, the shader caches and a private Mesa cache) and gets the same scripted input. Capture: APS5_TRACE_SECONDS=1150:300, a static scene in play.

    • The reported heap budget is pinned. The VK_EXT_memory_budget heap budget varies by about 1 GiB from run to run on this machine, with other VRAM users, and moves the storage budget as much as the cap does. So a private override, APS5_DIAG_HEAP_BUDGET_MIB (not part of this PR), sets the value both budget functions read. The device's real memory use is not pinned.

      pinned budget order arm storage budget, median presents frame p50 ms GPU busy ms/present
      6900 MiB 1st without 1725 MiB 288 982.5 454.3
      6900 MiB 2nd with 2769 MiB 363 813.4 368.0
      6900 MiB, repeat 1st with 2781 MiB 436 670.6 358.1
      6900 MiB, repeat 2nd without 1725 MiB 352 775.0 434.0
      6300 MiB 1st with 1928 MiB 293 1031.3 445.6
      6300 MiB 2nd without 1575 MiB 282 1002.1 495.1
    • At 6900 MiB this change gives about 1 GiB more storage budget, +24-26 % presents, -13 to -17 % frame p50 and -17 to -19 % GPU busy, in both orders. Other workers' compilations overlapped the first pair's "without" arm; the repeat is clean.

    • At 6300 MiB the reserve, not the cap, limits the storage cache. The difference is within noise. This change helps only where the budget has room above a quarter.

    • 0 amdgpu kernel events in every arm. Both traces of the repeat dropped some queue-worker events, so their phase totals are partial.

    • Summary JSON lines of the repeat pair (without, with):

{"commit":"e3f17c704367","dirty":false,"env":{"APS5_DIAG_HEAP_BUDGET_MIB":"6900","APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"1150:300","APS5_TRACE_STORAGE_EVICTIONS":"1"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","cpu":"AMD Ryzen 9 7950X 16-Core Processor","cpu_threads":32,"memory_mib":127901,"title_id":"PPSA25636","title":"Echoes of the End","content_version":"01.012.000","resolution":"3840x2160","window":{"spec":"seconds 1150:300","first_present":1364,"ms":300087.277},"presents":352,"frame_ms":{"p50":775.028,"p90":1154.740,"p99":1301.805,"max":1403.409},"gpu_busy_ms_per_present":433.975,"worker_busy_pct":{"queue worker 0x0":99.723,"queue worker 0x20":97.974},"top_phases_ms_per_frame":{"Driver.Packet":1666.043,"Driver.Packet.dispatch":772.816,"Driver.Dispatch":709.540,"Driver.Packet.memory_wait":532.812,"Resources.Complete":458.329,"Vulkan.Dispatch":426.743,"Vulkan.Dispatch.shader_resources":416.452,"Graphics.Draw":135.628},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":27.402,"events_lost":2866049,"totals_full":false},"queue worker 0x20":{"events_full_after_s":100.531,"events_lost":510424,"totals_full":false}},"trace":"aps5-trace-e3f17c704367-4045031.json.gz"}
{"commit":"52893755e950","dirty":false,"env":{"APS5_DIAG_HEAP_BUDGET_MIB":"6900","APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"1150:300","APS5_TRACE_STORAGE_EVICTIONS":"1"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","cpu":"AMD Ryzen 9 7950X 16-Core Processor","cpu_threads":32,"memory_mib":127901,"title_id":"PPSA25636","title":"Echoes of the End","content_version":"01.012.000","resolution":"3840x2160","window":{"spec":"seconds 1150:300","first_present":2087,"ms":300438.095},"presents":436,"frame_ms":{"p50":670.574,"p90":824.048,"p99":955.553,"max":1028.867},"gpu_busy_ms_per_present":358.077,"worker_busy_pct":{"queue worker 0x0":99.629,"queue worker 0x20":97.404},"top_phases_ms_per_frame":{"Driver.Packet":1339.948,"Driver.Packet.dispatch":642.094,"Driver.Dispatch":585.365,"Driver.Packet.memory_wait":397.702,"Vulkan.Dispatch":300.833,"Resources.Complete":299.066,"Vulkan.Dispatch.shader_resources":289.410,"Recorder.Submit":120.745},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":20.381,"events_lost":3772368,"totals_full":false},"draw committer":{"events_full_after_s":244.882,"events_lost":58767,"totals_full":false},"queue worker 0x20":{"events_full_after_s":79.566,"events_lost":731583,"totals_full":false}},"trace":"aps5-trace-52893755e950-3489203.json.gz"}
  • Superseded: the first unpinned pair (296 -> 557 presents) and an alternating four-arm repeat. Both were confounded by the reported budget, which was 485-640 MiB higher in every "with" arm; see the comments.

  • Traces, digests and comparisons: https://github.com/JJenkx/AnyPS5/releases/tag/storage-half-evidence

  • Independent, attributed, limited: @Rajkamalcode measured Squirrel with a Gun (PPSA22102) on an RTX 3070 8 GB under Windows. The base is an old one that runs the title, not current main. They used their own pinned 6900 MiB budget and one quarter -> half pair:

    • storage budget 1725 -> 3450 MiB;
    • storage images created 42,150 -> 3,000;
    • creation CPU 44,672 -> 2,471 ms.

    They make no frame-rate claim, because of long zero-present intervals, and their free-running A/B was inconclusive because the reported budget moved by several GiB. Details: perf(agc): let the storage image cache take up to half of the heap budget #10044 (comment)

  • Diagnostic-only occupancy runs (private overrides): with a 4096 MiB storage budget, Echoes in play held about 4077 MiB of storage images, with 250 MiB of sampled textures and about 2.2 GiB outside both caches. Its storage working set is about 3.5-4 GiB.

Checklist

  • Based on current main; no other open PR implements the same functions. fix(agc): grow the storage cache budget while it evicts under pressure #6194 grows the storage budget while it evicts, but keeps at least 2 GiB of headroom, so on this 8 GB GPU near its budget it does not grow.
  • 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 unimplemented path added)
  • 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 (not applicable)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: PPSA25636 (performance)
  • GPUs tested: AMD Radeon RX 6650 XT, RADV Mesa 26.2.4, Linux 7.2.9
  • AI-assisted: yes

🤖 Generated with Claude Code

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

Reviewed exact head 511ef5dd6a92cbb4ce9073497aac07605337d70b. The implementation changes only the storage-cache cap from the prior quarter-budget / quarter-heap-derived cap to min(heap / 2, budget / 2). The existing headroom formula, sampled-texture reservation, pressure response, fallback, and 512 MiB floor remain unchanged. The revised unit cases cover both caps, the pressure floor, monotonic shrinkage under added non-cache use, and the reserved headroom invariant. Current-main sync is clean (1 ahead / 0 behind), git diff --check passes, and hosted eligibility, intake, conventions, labels, and report checks pass. Product build/test jobs are skipped by repository policy.

I could not execute the Vulkan test locally: CMake configuration stops because SDL2 is uninitialized in this checkout and the host lacks XRandR development support. The author reports 15/15 focused Vulkan tests on the rebased patch, including a test-only negative control that fails on main. I downloaded the linked release summaries and traces and reran tools/performance_compare.py; it reproduces the reported 296 to 557 presents, p50 1018.7 to 529.3 ms, and GPU busy 466.6 to 277.7 ms per present. The comparison flags window.first_present as the only recorded configuration difference; the PR explains both measured windows begin in the same static scene. Both traces dropped detailed events, so phase attribution is partial, while the summary frame/GPU metrics remain available. I have not independently run the title or verified the physical scene.

One non-blocking maintainability note: the local conventions checker emits a debt-length warning for the updated texture-budget entry (1,522 characters); the hosted conventions check passes. Trimming it to the essential contract and evidence would clear the warning.

@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I condensed the texture cache budget debt entry to 903 characters in docs(agc): condense the texture cache budget debt entry, and tools/check_conventions.py --base origin/main now passes with no warnings. The entry keeps the contract (reserve, both halves, the floors, the once-a-second read and the trim at the next insert), the burst caveat, and where each value was checked. Docs only: no source or test change, so the gate and the pair still apply.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

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 1395 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 2434 docs/dev/debt/libSceAgcDriver.md
PR 4040 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 4448 docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5441 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5925 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6186 docs/dev/debt/libSceAgcDriver.md
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
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6202 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
docs/dev/debt/libSceAgcDriver.md
PR 6235 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6273 docs/dev/debt/libSceAgcDriver.md
PR 6280 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6412 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6481 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6548 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6808 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 7256 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7307 docs/dev/debt/libSceAgcDriver.md
PR 7410 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7484 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7604 docs/dev/debt/libSceAgcDriver.md
PR 7618 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7689 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7844 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7910 docs/dev/debt/libSceAgcDriver.md
PR 8014 docs/dev/debt/libSceAgcDriver.md
PR 8223 docs/dev/debt/libSceAgcDriver.md
PR 8246 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8319 docs/dev/debt/libSceAgcDriver.md
PR 8365 docs/dev/debt/libSceAgcDriver.md
PR 8542 docs/dev/debt/libSceAgcDriver.md
PR 8592 docs/dev/debt/libSceAgcDriver.md
PR 8998 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 9348 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9356 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9387 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9393 docs/dev/debt/libSceAgcDriver.md
PR 9400 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 9431 docs/dev/debt/libSceAgcDriver.md
PR 9432 docs/dev/debt/libSceAgcDriver.md
PR 9450 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9508 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9592 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9672 docs/dev/debt/libSceAgcDriver.md
PR 9673 docs/dev/debt/libSceAgcDriver.md
PR 9674 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9675 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9676 docs/dev/debt/libSceAgcDriver.md
PR 9700 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9748 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9842 docs/dev/debt/libSceAgcDriver.md
PR 9891 docs/dev/debt/libSceAgcDriver.md
PR 9892 docs/dev/debt/libSceAgcDriver.md
PR 9893 docs/dev/debt/libSceAgcDriver.md
PR 9895 docs/dev/debt/libSceAgcDriver.md
PR 9897 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9898 docs/dev/debt/libSceAgcDriver.md
PR 9904 docs/dev/debt/libSceAgcDriver.md
PR 9905 docs/dev/debt/libSceAgcDriver.md
PR 9906 docs/dev/debt/libSceAgcDriver.md
PR 10193 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 10348 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp

@github-actions github-actions Bot added docs agc libSceAgc* labels Oct 23, 2026
@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the evidence in the description: the matched pair above does not isolate this change. I am returning the PR to draft until a repeated, alternating pair does.

A later run of the same commit as the "with" arm (5ae92fcc38), with the same seed, inputs and window, measured: seconds 1150:300 from present 1350: 265 presents, frame p50 1083.3 / p90 1450.5 / p99 1606.3 / max 1842.4 ms, GPU busy 526.0 ms per present. That is close to the "without" arm (296 presents), not to the "with" arm (557).

The game's own budget lines ([gpu] sampled texture cache budget …) show why. They report the VK_EXT_memory_budget heap budget, which depends on other VRAM users on the machine:

run commit reported budget (MiB) sampled textures (MiB) storage budget (MiB) presents in window
pair, without 0f8d65651b 6554-6954 1077-1250 1530-1738 296
pair, with 5ae92fcc38 7164-7170 647-707 2425-3046 557
repeat, same code as "with" 5ae92fcc38 6237-6604 825-1174 1326-2009 265

So the outcome depends on the reported budget at run time as much as on the cap. When the budget is lower, the storage cache stays under its working set. It keeps evicting, the sampled cache grows with copies of re-created storage images, and the reserve shrinks the storage budget further. The mechanism in the description still holds. This PR raises the ceiling only when the budget has room, and the first pair happened to run with about 1 GiB more budget on the "with" side.

Next: alternating arms (without, with, with, without, ...). Each arm will log the reported budget and storage occupancy, and the evidence will compare arms with similar budgets. I will update the description with that result, or close the PR if the gain does not hold.

@JJenkx
JJenkx marked this pull request as draft October 23, 2026 09:45
@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Alternating repeat, as planned in the correction above. Same seed and inputs as before, gameplay window APS5_TRACE_SECONDS=1150:300, order without, with, with, without. Each arm logged the reported heap budget and storage occupancy (private diagnostics, not part of this PR).

arm commit reported budget, median (MiB) storage budget, median (MiB) sampled textures, median (MiB) presents frame p50 ms GPU busy ms/present
w1 0f8d65651b 6342 1573 1190 274 1084.8 481.9
c1 5ae92fcc38 6982 2973 499 655 459.3 241.4
c2 5ae92fcc38 6836 2857 513 485 630.3 313.5
w2 0f8d65651b 6351 1572 1118 244 1179.1 496.1

There were 0 amdgpu kernel events in every arm.

Both arms with this change were faster than both arms without it. In all four arms, however, the reported budget was higher on the side with the change (by 485-640 MiB), and an earlier run of the same change at a median budget of about 6.1 GiB was slow (265 presents). So these runs still do not separate the cap from the budget. They are consistent with this: the change helps when the budget leaves room for about 2.8 GiB or more of storage images, and does not help below that.

To isolate the cap, the next pair pins the reported heap budget that the caches read to the same value in both arms. It uses a private diagnostic override and runs at 6900 MiB and at 6300 MiB. The PR stays a draft until that result is in.

@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Budget-pinned result. Both arms read the same reported heap budget: a private diagnostic override (APS5_DIAG_HEAP_BUDGET_MIB, not part of this PR) replaces the VK_EXT_memory_budget value that the cache budgets read. The rest is unchanged from before: gameplay seed, inputs, APS5_TRACE_SECONDS=1150:300, one build directory. The arms are the integration tree without this change (e3f17c7043) and with it (52893755e9), both carrying the same diagnostic commits. The device's real memory use was not pinned and still varies.

pinned budget arm storage budget, median real heap use, median presents frame p50 ms GPU busy ms/present amdgpu events
6900 MiB without 1725 MiB 5252 MiB 288 982.5 454.3 0
6900 MiB with 2769 MiB 6033 MiB 363 813.4 368.0 0
6300 MiB with 1928 MiB 5395 MiB 293 1031.3 445.6 0
6300 MiB without 1575 MiB 5219 MiB 282 1002.1 495.1 0

With equal budget input, this change is a modest, budget-dependent gain, not the 1.9x of the first pair:

  • At 6900 MiB it raises the storage budget by about 1 GiB, with +26 % presents and -17 % frame p50.
  • At 6300 MiB the reserve, not the cap, limits the storage cache. The difference there is within noise (+4 % presents, +3 % p50).

Caveat on the 6900 MiB pair: other workers' compilations overlapped its "without" arm, which may have slowed it. I will repeat that pair on its own and report it. The PR stays a draft until then. I will rewrite the description's Tested section around the pinned pairs once the repeat is in, or close the PR if the 6900 MiB gain does not hold.

@Rajkamalcode

Copy link
Copy Markdown
Contributor

Independent 8 GB NVIDIA/Windows check: pinned 6900 MiB

I measured this on Squirrel with a Gun (PPSA22102, content 01.000.000, RTX 3070 8 GB, Windows 11 22635, driver 32.0.16.1088) on the old base that still runs this title. Current main still crashes against this title's guest libkernel/libc (libkernel.prx+0xe047), so the tree is old base b6dd2f9e3 plus 321460a4e (the live-sampled reserve formula) and this PR's cap line; the control is the same tree with the existing quarter cap. This is not a current-main validation.

Diagnostic only: a private local APS5_DIAG_HEAP_BUDGET_MIB=6900 replaces the reported heap budget read by both cache-budget functions; real heap use is unchanged. It is not part of this PR and I am not proposing it upstream. Both runs: 360 s, stock ini 0156186E, same pad schedule, no texture-budget override, no fatal/OOM.

Pinned 6900 MiB, quarter -> half (single pair, not ABBA)

arm storage budget cached storage (tail) storage images storage MiB create ms images phase ms builds
quarter 1725 MiB (1458-1725) 1414-1717 MiB 42,150 472,365 44,672 113,150 128,035
half 3450 MiB (2706-3450) 2699-3038 MiB 3,000 26,270 2,471 10,652 231,503

Normalized per build:

arm images/build create ms/build images phase ms/build
quarter 0.329 0.349 0.884
half 0.0130 0.0107 0.0460

The half cap creates 92.9% fewer storage images over the run, 25x fewer per build, and cuts creation CPU by roughly 33x per build. The storage budget moves from 1725 MiB to 3450 MiB, above the ~2.5 GB working set this title needs; the quarter cap stays below it. The half arm had a long 0-present interval and then ended at 18-21 presents/10 s; the quarter arm ended at 0. Builds continued through the 0-present intervals, so I do not use final presents as an FPS result.

Free-running host is not a valid A/B here

Four earlier 360 s runs, ABBA (quarter, half, half, quarter), same tree without the diagnostic pin:

run arm initial storage budget storage images create ms images phase ms builds
38 quarter 1812 MiB 47,900 63,811 138,144 105,091
39 half 3625 MiB 22,500 33,808 84,203 121,489
40 half 3625 MiB 48,050 53,630 131,319 105,751
41 quarter 1812 MiB 29,500 73,153 161,315 73,713

VK_EXT_memory_budget fell from 7250 MiB to 3798-6414 MiB during these runs, and the formula hit its 512 MiB floor in every arm. Run39 briefly held 85-87 presents/window before collapsing; run40 behaved like the control. Builds differ by up to 1.6x, so this host cannot separate the cap from the budget swings.

A separate fixed-2560 diagnostic (APS5_TEXTURE_CACHE_MIB=2560, baseline prx) produced 3,150 storage images / 24,780 MiB, create 2,463 ms, images phase 10,152 ms, 210,354 builds, with 18-19 presents/window for ~18 windows and then 0 for the last four. It confirms the ~2.5 GB working set but is not a clean FPS reference either.

Conclusion

At a stable 6900 MiB reported budget, the cap behaves as this PR describes: 1725 -> 3450 MiB storage budget puts SWAG's working set in cache and removes nearly all storage-image churn. That supports the author's budget-pinned methodology and the cap on a second 8 GB NVIDIA/Windows title. It does not establish a universal frame-rate gain, and it does not test #6194's adaptive growth. The free-running result is inconclusive because the reported budget itself moves by several GiB.

I can repeat the reverse order at 6900 MiB when the GPU is free; the current pair is one direction and the host is now handed back.

AI-assisted: yes. The tables above are measured; the conclusion is interpretation.

@JJenkx
JJenkx marked this pull request as ready for review October 23, 2026 17:35
@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @Rajkamalcode for the independent pinned-budget check on Squirrel with a Gun. I added it to the description as attributed, limited support, with your limits: old base, a single pair, no frame-rate claim, and the free-running A/B inconclusive. The description now rests on budget-pinned pairs on Echoes, with a repeat in reverse order. At 6900 MiB they show about +25 % presents and -13 to -17 % frame p50. At 6300 MiB they show no difference, because the reserve, not the cap, limits storage there. The earlier unpinned results are kept only as superseded. Marked ready.

@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Provenance note on the 6900 MiB repeat pair in the description. Two 4-thread library builds by another worker ran on this machine from 16:53:42 to 17:01:16 UTC. That overlapped the loading phase of the repeat's "with" arm (run start 16:44:45 UTC). Its capture window was 17:03:55-17:08:55 UTC, after the builds ended. The "without" arm (window 17:28:34-17:33:34 UTC) did not overlap at all. Both arms' 1140 s screenshots show the same scene in play. The first 6900 MiB pair's "without" arm did overlap other compilations, as the description says, so the repeat is the cleaner pair.

@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

An interaction between #8672 and #10044, from reading both diffs. It is not measured.

#8672's storageBudget(context, required) bounds the reclaim target with its own ceiling, min(TextureCacheBudget, heapBudget / 4). That is the quarter cap StorageTextureBudget used before #10044. #10044 raises the cap inside StorageTextureBudget to min(heap size / 2, heapBudget / 2) but does not touch #8672's separate ceiling. With both merged:

  • When the reserve formula already allows more than a quarter, needed = min(required, ceiling) is at or below cached.budget, so ReclaimIdleDevice never runs.
  • Reclaim can then only help a storage budget that is still below a quarter of the reported budget.

On Echoes of the End (8 GB GPU), the formula gives about 1.6-2.9 GiB of storage depending on the budget, so the quarter ceiling (about 1.6-1.75 GiB) would usually keep #8672 from reclaiming when #10044 is present.

Whichever PR lands second could derive the ceiling from the same cap expression as StorageTextureBudget, for example a small shared helper, so the two cannot disagree. I am measuring #8672 on its own on Echoes (fresh integration tree without #10044, reported budget pinned to 6900 MiB) and will post the result on #8672.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@JJenkx ShaderResources.cpp:1232–1238 still reserves only TextureCacheBudget for storage, while the new storage cap is half the heap/budget. The caches can then exceed the reported budget: an 8 GiB heap with 7.2 GiB budget and 2 GiB other use allows about 3.6 GiB storage plus 2.3 GiB sampled. Could the sampled budget reserve the storage cache’s actual size or its new cap, and could storageTextureBudgetTests assert the sum of both budgets stays within the reported budget?

JJenkx and others added 3 commits October 23, 2026 17:59
…dget

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JJenkx

JJenkx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean agreed; fixed in 9904c737ca (fix(agc): reserve the storage images held in the sampled texture budget), rebased on main 1b5efcc8d9.

  • SampledTextureBudget takes the storage images held as a new argument (storageBytes, default 0). It reserves max(TextureCacheBudget, storageBytes) for them instead of TextureCacheBudget alone, and sampledBudget() passes storageCacheBytes(). A small storage cache keeps the old reservation; a large one now shrinks the sampled budget by what it actually holds.
  • Your example (8 GiB heap, 7373 MiB budget, 2 GiB other use, 3686 MiB of storage images): the sampled budget is now its 2 GiB floor, not about 2.3 GiB. This is a test case.
  • New joint sweep in storageTextureBudgetTests: 8 and 16 GiB heaps, every budget, other use, and held sampled and storage sizes in 512 MiB steps. Above their floors, neither budget plus the other cache's held bytes exceeds the reported budget less its eighth. A model of both formulas over the same grid (24,714 cases) found no violation before I wrote the test.
  • The remaining overshoot is the existing 2 GiB sampled floor and the 512 MiB storage floor. A heap whose other users leave less than that still gets the floors, as on main. The debt entry now says so.

Exact-head gate (9904c737ca), same command as the description:

arm result layer inserted VUIDs
main 1b5efcc8d9 16/16 4 tests 0
this PR with the new reservation reverted to TextureCacheBudget(memory) 15/16: fails "a storage image cache larger than TextureCacheBudget was not reserved in full" 4 tests 0
this PR 16/16 4 tests 0

Logs: vvl10044-9904-*.log in https://github.com/JJenkx/AnyPS5/releases/tag/storage-half-evidence

The Echoes and Squirrel with a Gun numbers in the description predate this commit. This commit only lowers the sampled budget when the storage cache holds more than TextureCacheBudget; it does not change the storage budget.

@JJenkx
JJenkx force-pushed the perf/agc-storage-cache-half-heap branch from a2f89ca to 9904c73 Compare October 23, 2026 23:52
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@JJenkx full CI on 9904c73 (run 40048597992) fails agc_driver_sampled_cache_budget_memory on Linux:

AGC graphics: 52 GiB free of a 64 GiB budget does not leave the storage image cache its quarter

You updated the unit check in tests/Graphics.cpp to the new half, but the execution test tests/execution/SampledCacheBudget.cpp (around line 1053, ReadStorageBudget(context) == quarter) still expects a quarter. Could you move that expectation (and its message) to the half as well, then rerun agc_driver_sampled_cache_budget_memory on a Vulkan device? The Windows failure in the same run (agc_write_tracking_metadata_commit, "an untouched leaf has a write stamp") has shown up on an unrelated PR before, so I don't think it's yours.

…cution test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JJenkx

JJenkx commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks, fixed in 9fc13424e8 (test(agc): expect half of the heap or budget in the storage cache execution test). tests/execution/SampledCacheBudget.cpp now expects min(largest device-local heap / 2, ReportedBudget / 2), and the message is updated. Its ExpectedBudget also passes the held storage bytes to SampledTextureBudget, as production now does. No other test references either budget rule.

Exact-head gate on Linux / RX 6650 XT / RADV, same command as before plus agc_driver_sampled_cache_budget_.*. Results: main 1b5efcc8d9 18/18; this PR with the new reservation reverted 17/18, failing only the intended agc_driver_graphics assertion; this PR 18/18, including agc_driver_sampled_cache_budget_memory. The layer was inserted in 6 tests per arm, with 0 VUIDs. Logs: vvl10044-9fc1-*.log in https://github.com/JJenkx/AnyPS5/releases/tag/storage-half-evidence

I agree the Windows agc_write_tracking_metadata_commit failure is unrelated: this PR touches only the texture cache budgets.

@oneandonlydean
oneandonlydean merged commit 564de82 into boykopovar:main Oct 24, 2026
10 checks passed
@Rajkamalcode

Copy link
Copy Markdown
Contributor

Post-merge check on current main: SWAG now runs again

Current main 41649e411 (includes merge 564de82a00d9a6a5d9dd1f2e4b125a4e358470bd and the final test fix 9fc13424e8) runs Squirrel with a Gun again with guest libs rebuilt from the same commit. The old libkernel.prx+0xe047 mismatch is gone. This is a post-merge check, not an attribution of the whole change to #10044.

Hardware/title: Ryzen 5 5600X, 16 GB RAM, RTX 3070 8 GB, Windows 11 22635, driver 32.0.16.1088; SWAG PPSA22102, content 01.000.000, guest 3840x2160 presented at 1060x596; stock ini 0156186E, same pad schedule as the earlier SWAG runs. Two 360 s runs, no texture-budget override. Guest libs are the current-main libs build; driver SHA-256 92858F8FC5838E08372E718CDB078613A604640122DC37EAAF7E4D36A61CD4CF.

Measured (no fatal/OOM in either run)

run storage images created images phase builds presents after load storage budget
48 385 2,182 ms 144,016 40-41 per 10 s 3,409-3,625 MiB
49 369 1,375 ms 132,005 44-45 per 10 s 3,227-3,625 MiB

The storage cache held 3,132 MiB / 2,857 MiB at the tail; the sampled budget reached its 2,048 MiB floor. Storage hits were 1,004,617 / 1,151,183 with 47,077 / 53,311 own-object refreshes over 360 s. Neither run had a zero-present window after the initial load. For scale, the old-base free-running controls on this title created roughly 42k-48k storage images, and the old fixed-2560 MiB diagnostic created 3,150 images with a 10,152 ms images phase.

AI-assisted interpretation: the merged half cap is visibly active on this 8 GB NVIDIA/Windows node, and the current-main combination keeps the storage working set near 3 GiB without the collapse to the 512 MiB floor seen on the old base. I am not claiming the images-phase or presentation change as #10044 alone: current main includes #8672 and many other merged changes. I also am not using presents as an FPS result, since the builds and scene progression differ between runs.

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 unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants