Skip to content

perf(shader): reuse an evicted memo result's bindings for the next miss - #9049

Closed
EdouardSence wants to merge 4 commits into
boykopovar:mainfrom
EdouardSence:perf/recycle-evicted-result-bindings
Closed

EdouardSence wants to merge 4 commits into
boykopovar:mainfrom
EdouardSence:perf/recycle-evicted-result-bindings

Conversation

@EdouardSence

Copy link
Copy Markdown
Contributor

What

In ASTRO BOT's Sky Garden, the queue worker thread is at ~99 % and bounds the frame. The Recompile(request, capture) result memo hits only ~6.5 % of the time there: about 35k misses per second, nearly every one evicting an entry. Every miss builds a new RecompileResult. Its bindings are copied from the binding plan with all of each DescriptorBinding's vectors, and the evicted result is destroyed with all of its vectors on the same thread. In an eu-stack sample of the worker (120 samples), ~18 % of samples were in ~RecompileResult under materializePreparedMemoized, and malloc/free showed up under Materialize and Populate too. With LD_PRELOAD of jemalloc the same build ran ~8 % faster, so allocation is a real share of the frame there.

  • On a miss, materializePreparedMemoized now picks the set's least recently used entry before materializing. When the memo holds the only reference to it (use_count() == 1 under the source mutex, which every other lookup also takes), the entry is taken out of the set and its bindings vector is passed to materializeResult.
  • DescriptorBindingBuilder::Populate copy-assigns each plan binding into the allocation's existing elements, then resizes to the plan's size, instead of building a new vector. The vectors in a reused element keep their storage. An empty allocation gives the same result as before.
  • Results with other owners (a queued draw still holding one) are evicted as before.
  • The eviction counter of APS5_PROFILE_DRAW's [recompile] result memo line also counts these entries.

Tested

  • Linux (CachyOS, kernel 7.2.10), GCC, -DBUILD_TESTING=ON: ctest 1786/1792 with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 on the RX 9070 XT. The 6 failures (agc_driver_guest_memory_lifetime, agc_driver_image_array_window_past_layer_limit, agc_driver_tessellation (segfault), net_dontwait_blocking_send, _sendto, _sendmsg) fail the same way on main 2abfb1b in this environment.
  • Focused tests with ctest -V and the same variables, on main and on this branch: agc_shader_disk_cache, agc_shader_memory, agc_driver_graphics, agc_driver_tests and the agc_driver_draw* tests (17 tests) pass on both. Neither log has a VUID- line or was not found but was requested by env var VK_INSTANCE_LAYERS.
  • verifyBindingPlanSelection (agc_shader_disk_cache) now also populates the selected plan into an allocation that still holds the full plan's four bindings, and checks that the result equals a fresh populate.
  • Speed: ASTRO BOT PPSA21567 01.019.000, Sky Garden (-lvl underwater_aerial_garden), headless gamescope 3840×2160, no input, APS5_TRACE_SECONDS=90:50, APS5_NO_SNAPSHOT_CHECK=1. Both builds also ran with APS5_HOST_IMPORT_MIB=11815, because main without fix(agc): keep at most 4 GiB of the host heap from the host import budget #7844 thrashes textures in this level on a 16 GB card. Each build had a warm-up run, then the runs were interleaved.
Build presents (50 s) frame p50 / p90 / p99 ms GPU busy ms/present Driver.Draw.shader_compile_and_link ms/frame
main 2abfb1b, warm-up 194 252.2 / 272.0 / 350.6 131.1 140.0
this PR 7fcf715, warm-up 204 240.3 / 258.5 / 266.8 130.3 113.3
main, run 1 192 255.5 / 273.0 / 379.8 130.3 137.2
this PR, run 1 209 237.5 / 254.8 / 260.8 131.3 110.3
main, run 2 192 260.2 / 273.7 / 402.2 130.4 138.7
this PR, run 2 208 237.2 / 254.6 / 260.3 129.9 110.3

This PR is about 8 % faster at p50 here, and p99 drops from 380-402 ms to ~261 ms. GPU time is unchanged. In the traces, Shader.DrawStage goes from 123.4 to 96.8 ms per frame. The worker stays at ~99 %, so the draw cost still bounds the frame.

Summary lines (run 1 of each). Traces of both run 1s: main, this PR. Both traces dropped events after a few seconds on the worker (trace_dropped); the frame times come from the presents.

{"commit":"2abfb1beb4dd","dirty":false,"env":{"APS5_HOST_IMPORT_MIB":"11815","APS5_NO_SNAPSHOT_CHECK":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"90:50"},"gpu":"AMD Radeon RX 9070 XT (RADV GFX1201)","driver":"radv Mesa 26.2.4-arch3.1","os":"Linux 7.2.10-1-cachyos","cpu":"AMD Ryzen 7 7800X3D 8-Core Processor","cpu_threads":16,"memory_mib":31228,"title_id":"PPSA21567","title":"ASTRO BOT","content_version":"01.019.000","resolution":"3840x2160","window":{"spec":"seconds 90:50","first_present":1601,"ms":50091.668},"presents":192,"frame_ms":{"p50":255.485,"p90":273.049,"p99":379.795,"max":562.272},"gpu_busy_ms_per_present":130.340,"worker_busy_pct":{"queue worker 0x0":99.321,"queue worker 0x40":99.504,"queue worker 0x48":99.349,"queue worker 0x46":0.840,"queue worker 0x3a":0.078},"top_phases_ms_per_frame":{"Driver.Packet":779.517,"Driver.Packet.memory_wait":492.248,"Driver.Packet.draw":198.854,"Driver.Draw":197.899,"Graphics.Draw":154.105,"Driver.Draw.shader_compile_and_link":137.224,"Graphics.Draw.draw_and_resource_release":133.193,"Shader.DrawStage":123.446},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":3.259,"events_lost":3623330,"totals_full":false},"draw committer":{"events_full_after_s":9.812,"events_lost":1080694,"totals_full":false}},"trace":"aps5-trace-2abfb1beb4dd-758987.json.gz"}
{"commit":"7fcf715559eb","dirty":false,"env":{"APS5_HOST_IMPORT_MIB":"11815","APS5_NO_SNAPSHOT_CHECK":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"90:50"},"gpu":"AMD Radeon RX 9070 XT (RADV GFX1201)","driver":"radv Mesa 26.2.4-arch3.1","os":"Linux 7.2.10-1-cachyos","cpu":"AMD Ryzen 7 7800X3D 8-Core Processor","cpu_threads":16,"memory_mib":31228,"title_id":"PPSA21567","title":"ASTRO BOT","content_version":"01.019.000","resolution":"3840x2160","window":{"spec":"seconds 90:50","first_present":898,"ms":50224.935},"presents":209,"frame_ms":{"p50":237.486,"p90":254.840,"p99":260.847,"max":262.016},"gpu_busy_ms_per_present":131.252,"worker_busy_pct":{"queue worker 0x0":99.238,"queue worker 0x40":99.617,"queue worker 0x48":99.519,"queue worker 0x46":0.908,"queue worker 0x3a":0.140},"top_phases_ms_per_frame":{"Driver.Packet":718.817,"Driver.Packet.memory_wait":448.867,"Driver.Packet.draw":179.435,"Driver.Draw":178.494,"Graphics.Draw":145.812,"Graphics.Draw.draw_and_resource_release":125.790,"Driver.Draw.shader_compile_and_link":110.315,"Shader.DrawStage":96.817},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":3.773,"events_lost":3198830,"totals_full":false},"draw committer":{"events_full_after_s":9.461,"events_lost":1128611,"totals_full":false}},"trace":"aps5-trace-7fcf715559eb-760875.json.gz"}
  • Not tested: Windows, NVIDIA, Intel, other titles.

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: AMD Radeon RX 9070 XT, RADV Mesa 26.2.4, Linux 7.2.10 (CachyOS)
  • AI-assisted: yes

Tested and written by Claude (AI assistant) on a contributor's machine.
AI-assisted: yes

🤖 Generated with Claude Code

A result memo miss now takes the least recently used entry of its set
before materializing when the memo holds the only reference to it, and
Populate copy-assigns the new bindings into that result's binding
elements, so their vectors keep their storage instead of being freed
and allocated again on every miss.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the perf Performance change label Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 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 5021 core/shader/recompiler/Recompiler.cpp
PR 5315 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 5371 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 5609 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6280 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.cpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6539 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.cpp
PR 6612 core/shader/recompiler/Recompiler.cpp
PR 6858 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6924 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6959 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7085 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 7201 core/shader/recompiler/Recompiler.cpp
PR 7223 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 7395 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 7410 core/shader/recompiler/Recompiler.cpp
PR 7484 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 7522 core/shader/recompiler/Recompiler.cpp
PR 7642 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.cpp
PR 7684 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7689 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 7694 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 8004 core/shader/recompiler/Recompiler.cpp
PR 8077 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 8939 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 9020 core/shader/recompiler/Recompiler.cpp
PR 9183 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 9193 core/shader/recompiler/Recompiler.cpp
PR 9335 core/shader/recompiler/Recompiler.cpp
PR 9356 core/shader/recompiler/Recompiler.cpp
PR 9387 core/shader/recompiler/Recompiler.cpp
PR 9457 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 9508 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.cpp
PR 9633 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 9748 core/shader/recompiler/Recompiler.cpp
PR 9749 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 9790 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 9889 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 9897 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 9957 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 9969 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 9989 core/shader/recompiler/Recompiler.cpp
PR 10000 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 10049 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 10193 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.cpp
PR 10351 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 10359 core/shader/recompiler/Recompiler.cpp
PR 10489 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 10547 core/shader/recompiler/Recompiler.cpp
PR 10652 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 10846 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 10941 core/shader/recompiler/Recompiler.cpp
PR 10976 core/shader/recompiler/Recompiler.cpp
PR 11126 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp

@EdouardSence

Copy link
Copy Markdown
Contributor Author

Checked the files the overlap comment could not compare, against the diffs of those 25 pull requests. No other pull request changes the result memo (materializePreparedMemoized, ResultMemoEntry) or Populate's binding loop.

Four touch the line this PR changes in Recompiler.cpp, the materializeResult signature:

Each is a one-line textual conflict. Whichever lands second keeps both changes; the parameters are independent. No Depends on needed.

Tested and written by Claude (AI assistant) on a contributor's machine.
AI-assisted: yes

@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 21, 2026
@ycbuzi

ycbuzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Windows run of 7fcf715 merged onto main 745b394 (efc0236650, clean), the platform the description lists as not tested. Windows 11 Home 10.0.26200, MinGW-w64 GCC 15.2.0 (ucrt-posix-seh r7), Release, BUILD_TESTING=ON. RTX 4070 SUPER, NVIDIA 616.92 (ANYPS5_GPU=4070).

Built agc_shader_disk_cache_tests with no errors or warnings. ctest --repeat until-fail:2 on the 15 tests these executables register on Windows: all pass on both repeats. These are the agc_shader_disk_cache, agc_shader_cache_housekeeping_* and agc_shader_prebuilt_discovery_* tests. Not run: a title.

@teepean

teepean commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@EdouardSence a Linux/NVIDIA data point on Stray (PPSA02100). It was measured on my stack, not on plain main, with a replacement allocator, so take it as one data point.

Setup

  • CachyOS (kernel 7.2.10, scx_lavd), Ryzen 9 9950X, RTX 3090 (NVIDIA 615.78.08), GCC 16.2.1, Release.
  • Base: my stack on main 1ff5c8a (showcase/stray-stack, 27dbc28). 7fcf715 cherry-picks onto it cleanly.
  • Stray at the start of the Dead City chapter, 3840x2160, stock engine.ini, scripted input.
  • tcmalloc (gperftools 2.18) or jemalloc 5.4.0 preloaded, as a 2x2 with and without this PR, four rounds in a balanced order.

FPS (mean of four runs)

tcmalloc jemalloc
without #9049 17.93 18.57
with #9049 17.60 17.83
  • Across both allocators, the PR is -0.59 FPS (-3.2 %), and slower in all four rounds.
  • [recompile] result memo with APS5_PROFILE_DRAW=1 (one run each, tcmalloc, about 100k misses per 10 s in both): Populate takes 1.9 us per miss without the PR and 2.4 us with it.
  • Part of that difference is where the frees are counted. Without the PR, the evicted result is freed when the new one is inserted, outside Populate's timer.
  • My guess at the rest: GuestBuffers, GuestSamplers and ShaderData bindings still move-assign a freshly built vector, so their old storage is freed, not reused. The recycled entry is also the least recently used one, so its memory is likely cold in the cache. With tcmalloc or jemalloc, a fresh small allocation costs less than that.

Tests and output

  • agc_shader_disk_cache_tests passes, including the new check.
  • The focused draw tests I run on my stack give the same results with and without the PR.
  • Frames 26-39 of a 40-frame dump match the stack without the PR (mean difference at most 0.7).

Your -8 % came from main with glibc, where malloc and free cost more, so the result may well differ there. If it helps, I can measure main + #9049 with glibc on Stray.

AI-assisted: yes (text with Claude Code; the numbers are from my machine).

… binding's storage

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

Copy link
Copy Markdown
Contributor Author

@teepean thanks for the Stray runs and the reading. Your guess about the move-assigned vectors was right, so I pushed edc9564: GuestBuffersDescriptor and GuestSamplersDescriptor now fill the binding's existing guestDescriptor instead of returning a new vector, so a recycled binding keeps that storage too. ShaderData still moves its vector in.

Sky Garden A/B, ASTRO BOT PPSA21567 01.019.000, Ryzen 7 7800X3D, RX 9070 XT (RADV Mesa 26.2.4), CachyOS. Headless gamescope 3840x2160, no input, APS5_TRACE_SECONDS=90:50, APS5_HOST_IMPORT_MIB=11815, warm shader cache, all on base 2abfb1b. Two rounds, arms interleaved. Frame p50 in ms:

Build glibc r1 / r2 jemalloc 5.4.0 r1 / r2
main 267.8 / 259.8 239.2 / 232.5
previous head 7fcf715 246.2 / 251.2 240.7 / 220.6
new head edc9564 243.4 / 240.9 222.0 / 221.8

On this machine and scene, the previous head was not slower with jemalloc (its round 1 is the noisy one). The new head is about 8 % faster than main with glibc and about 6 % faster with jemalloc. Your Stray scene on NVIDIA may still behave differently, for example because the LRU victim is cold in the cache, as you said. If you have time, a run of the new head on your stack would show whether this change recovers the 3 %.

tools/check_conventions.py passes, and so do the 7 agc_shader_disk_cache*/housekeeping/prebuilt tests. #9177 (milescoviello) adds a reserve in GuestSamplersDescriptor, which this commit also touches, so whichever PR lands second needs a small rebase.

Tested and written by Claude (AI assistant) on a contributor's machine.
AI-assisted: yes

@ycbuzi

ycbuzi commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Windows run of edc9564, the update since my earlier run, merged onto main d6eafa5 (0edab22f56, clean). Windows 11 Home 10.0.26200, MinGW-w64 GCC 15.2.0 (ucrt-posix-seh r7), Release, BUILD_TESTING=ON. GPU: NVIDIA GeForce RTX 4070 SUPER, driver 616.92 (ANYPS5_GPU=4070, ANYPS5_REQUIRE_VULKAN=1).

Built libSceAgcDriver and agc_shader_disk_cache_tests with no errors or warnings. ctest --repeat until-fail:2 on the 15 tests that executable registers on Windows (agc_shader_disk_cache, agc_shader_cache_housekeeping_* and agc_shader_prebuilt_discovery_*): all pass on both repeats, none skipped. Not run: a frame-time measurement, a title.

@teepean

teepean commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

@EdouardSence thanks, here is edc9564 on our stack. Short version: the loss is gone; it is level with our base now.

Setup

FPS (8 rounds, order alternating)

runs mean
base 18.3 18.2 18.0 17.8 18.5 18.3 18.6 18.6 18.29
base + 7fcf715 + edc9564 17.8 18.8 17.8 18.3 19.1 18.0 18.9 18.1 18.35 (+0.3 %)

The PR side won 4 of 8 rounds. Run-to-run spread here is 0.3-0.5 FPS, so this is level within noise. Filling the reused binding's storage removed what cost us before.

Checks

  • Our verify run (proof memo, draw snapshots and stage-instance results checked against the full paths): 6.22M memo hits, 0 wrong; 670,723 stage-instance results compared, 0 differing.
  • One run of the PR side ended at 184 s: the process exited with nothing in the log, no error or assertion. Its retry ran normally. I haven't seen that before on this stack, but one run is not enough to tie it to this PR. I'll mention it if it happens again.

AI-assisted: yes (text with Claude Code; the numbers are from my machine).

@EdouardSence

EdouardSence commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

@teepean thanks for the 8 rounds and the verify run (0 wrong memo hits, 0 differing stage results). Level on your committer-bound stack and faster on the worker-bound Sky Garden here is what I hoped the change would do.

About the exit at 184 s: none of my Sky Garden runs on either head exited early (8 runs today across the two heads, 180 s each). If it happens again, a core dump or the last [gpu] lines would show whether it's in the memo path.

Written by Claude (AI assistant) on a contributor's machine.
AI-assisted: yes

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Independent profiling lead from Outer Wilds (PPSA08102): the queue worker spends substantial CPU time allocating/destructing materialized shader results, so this PR targets a path worth comparing in that title.

Profile packet, perf report and terminal review. This is not a speedup measurement or validation of #9049: it is a clean older 05250e0006559b454701737066e14cc64b39b437 integration (main a6dc + #9406), without this PR. The reviewed screenshots show the outdoor waking scene rather than the Wake Up prompt; lighting correctness remains unverified.

The 60-second all-thread CPU profile has 6,333 samples and zero lost. The queue worker accounts for 22.82% of process CPU samples; within that thread, inclusive stacks include materializePreparedMemoized at 25.40% and operator new at 21.59%. These percentages overlap and are not frame-time percentages. This does not establish what fraction of allocations can be reused, and this PR still creates a new shared result object.

I've queued independent focused checks of current main d6e7ab42dfe1 versus main plus head edc95643a1f4. After those are reviewed, the next step is a matched title trace comparison; none has run yet. Sharing the hotspot evidence now to avoid duplicating this optimization.

@meh7an

meh7an commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

@EdouardSence I ran this on another title with the same pattern: Sonic Origins Plus (PPSA05324), Sonic 2 page of the main menu. On main there, the queue worker bounds the frame, and results built on memo misses plus their release take about half of its samples.

Windows 11 Pro 10.0.26300, NVIDIA GeForce RTX 2080 Ti, driver 617.14, i9-9900K. I rebased your two commits onto main e7f46f5 locally, to measure only. The one conflict is main's new mergeTargetImage prologue in materializeResult; I kept it together with your reuse parameter.

Settings: main's defaults, one warm-up boot per build, then three interleaved traced rounds with APS5_TRACE_SECONDS=200:15, interleaved with main and with #10423:

frame p50 (ms) Driver.Packet (ms/frame) Driver.Draw.shader_compile_and_link (ms/frame) GPU busy per present (ms)
main 114.8 / 82.4 / 80.4 111.9 / 85.1 / 80.2 57.6 / 41.1 / 41.2 67.9 / 68.1 / 68.1
this PR 80.2 / 79.3 / 80.1 78.8 / 79.7 / 78.5 37.3 / 35.9 / 37.1 70.4 / 67.7 / 68.4
  • The median frame p50 goes from 82.4 to 80.1 ms (-2.9 %), and the compile-and-link phase from 41.1 to 37.1 ms (about -10 %).
  • main's first round was a slow window (114.8 ms), so I would not count it.
  • Full ctest (2062 tests) on main plus this PR: everything passes except the three agc_driver_sample_order* tests, which fail the same way on plain main on this machine.

#10423 changes materializePreparedMemoized too: it serves most of these misses by patching a cached result. The two should add up, because this PR makes the remaining misses cheaper. Whichever lands second will need a small rebase. I'm happy to do the rebase on my side, or to rerun the pair once both are on main.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@EdouardSence #8564 landed and now conflicts with this PR in Recompiler.cpp, at the start of materializeResult. Main renamed the snapshot parameter to captured and added a mergeTargetImage prologue, which appends the color-target-merge image to a copy of the snapshot for target-merge variants. Your change adds the reuse parameter to the same signature. The fix should be small: keep main's prologue and captured parameter, add std::vector<DescriptorBinding> reuse = {} after registrationDefaults, and keep bindings.bindings = std::move(reuse); as it is. One thing to check: a target-merge result binds one more image than the snapshot it came from, so make sure a recycled vector is cleared or overwritten, and never assumed to have the right length, before it's filled. Could you merge main, then rerun the memo-reuse tests and agc_shader_disk_cache?

A heads-up for after that: #10423 (meh7an) also changes materializePreparedMemoized, so whichever of the two lands second will need a small rebase there. meh7an has offered to do it if theirs is second. It would help if you and meh7an agree on the order in one of the threads.

…result-bindings

# Conflicts:
#	core/shader/recompiler/Recompiler.cpp
@EdouardSence

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks, merged main aeeaefa in 012ec06, resolved as you described: main's mergeTargetImage prologue and captured parameter kept, std::vector<DescriptorBinding> reuse = {} added after registrationDefaults, bindings.bindings = std::move(reuse); unchanged.

  • Recycled vector length: DescriptorBindingBuilder::Populate does bindings.resize(plan.bindings.size()) and then assigns every element from the plan (physical = entry.descriptor) before filling its guest words, so a recycled vector is never used at its old length. A target-merge result's extra image comes from the merged snapshot through the plan, not from the recycled storage.
  • Tests: Linux (CachyOS), GCC, Release, -DBUILD_TESTING=ON, RX 9070 XT, RADV Mesa 26.2.4, with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1: ctest 2023/2024. The one failure, agc_driver_tessellation (segfault), fails the same way on main here, as listed in the PR body. The focused set with ctest -V (agc_shader_disk_cache, agc_shader_memory, agc_driver_graphics, agc_driver_tests and the agc_driver_draw* tests, 26 tests) passes, with no VUID- line in the log.

@meh7an thanks for the Sonic Origins Plus run, a second title and vendor with the same direction. On the order with #10423: #9049 is mergeable again now, so if it lands first I'm glad to take your offer to rebase there. If #10423 is ready first, I'll rebase this one onto it instead; either way works for me.

Written by Claude (AI assistant) on a contributor's machine.
AI-assisted: yes

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 24, 2026
danielradosa added a commit to danielradosa/AnyPS5 that referenced this pull request Oct 24, 2026
…ted memo result's bindings for the next miss): the next run is cold anyway after the d5f5cd8 merge changed the shader cache version
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 24, 2026
…result-bindings

# Conflicts:
#	core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
@EdouardSence

Copy link
Copy Markdown
Contributor Author

Merged current main c8d34aa into the branch, now at 391c514.

  • Conflict: one hunk in DescriptorBindingBuilder.cpp. perf(agc): drop three per-draw allocations on the draw and materialize paths #9177 added result.reserve(resources.size() * 4u) to GuestSamplersDescriptor, which this PR turns into a fill of the reused vector. The resolution keeps this PR's signature (result.clear()) and perf(agc): drop three per-draw allocations on the draw and materialize paths #9177's reserve, the same shape GuestBuffersDescriptor already has. No other file conflicted.
  • Conventions: check_conventions.py --base origin/main passes.
  • Tests on 391c514 (full ctest, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, RX 9070 XT, RADV): 2100/2101 pass. The one failure is agc_driver_tessellation (segfault), the same as on main in my previous run. The only VUID- lines in the log come from that test's instance and device creation.

I have not rerun the Sky Garden A/B on this merge; the conflict only changes where the reserve sits.

AI-assisted: yes

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 24, 2026
@meh7an

meh7an commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

@EdouardSence the overlap check pairs this PR with #10423 (masked result memo) and #10757 (recycling evicted results), which change materializePreparedMemoized too. #10757 does on the patched path what this PR does on the miss path: when the memo evicts a result nothing else references, it keeps the whole result in a small per-thread pool and copy-assigns the next patched result into it. So the two compete for the same evicted result.

Proposal: this PR goes first. Once it merges, I rebase #10423 and #10757 onto it and give them one reuse path: an evicted result goes to #10757's pool, and your miss path takes its bindings from a pooled result instead of evicting the victim early. That keeps your reuse working with both tables. #10423 and #10757 then declare Depends on: #9049. Does that order work for you?

@EdouardSence

Copy link
Copy Markdown
Contributor Author

@meh7an That order works for me: this PR first, then #10423 and #10757 rebased on it with one reuse path, where an evicted result goes to #10757's pool and the miss path takes its bindings from a pooled result. The branch still merges cleanly into current main da6c581. If a run of the combined stack on ASTRO BOT Sky Garden (RX 9070 XT, RADV) would help once you have it, tell me the heads and I'll post the numbers.

AI-assisted: yes

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 25, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@EdouardSence This conflicts with main in a way we can't resolve for you. Main's 54d7c78 ("perf(shader): reuse a memoized result by patching its copied words") rewrote the memo lookup and gave materializeResult a MemoPatch*, while this PR reuses an evicted result's bindings through a reuse vector in the same function. Could you merge main and decide whether this reuse still applies on top of the memo patching, and how? We'll review the update as a delta.

@EdouardSence

Copy link
Copy Markdown
Contributor Author

@oneandonlydean I merged main a5a780e locally and measured whether the reuse still pays on top of #10423. It doesn't, so I'm closing this PR.

  • After the merge, the reuse can only apply on the full-miss path (donor == nullptr): a patched result is a copy of its donor and never runs Populate. A full-miss result with a valid patch is also held by both tables, so evicting it from the exact table leaves use_count() == 2 and it is not recycled.
  • ASTRO BOT (PPSA21567) Sky Garden, RX 9070 XT, RADV, APS5_PROFILE_DRAW=1: main's memo now reports, per 10 s, about 94 % patched results and 80 to 160 misses, with Populate at 0.8 to 1.2 ms in total. That miss path is all this PR could make cheaper. Its original A/B (p50 −8 %) was taken before perf(shader): reuse a memoized result by patching its copied words #10423, when every memo miss ran Populate.
  • Three alternating rounds, APS5_HOST_IMPORT_MIB=11815: frame p50 283 to 296 ms on main, 276 to 291 ms with the merge, within the scene's spread.
  • The merge itself was sound: full ctest under the validation layer passed 2215/2217. The failures are agc_driver_tessellation (as on main) and progress_history, which expects English git output and fails under a French locale.

The cost left around eviction is the patched copy and the evicted result's free, which #10757 addresses. @meh7an the order question is moot then; nothing from this PR needs to carry into #10757.

AI-assisted: yes

@EdouardSence
EdouardSence deleted the perf/recycle-evicted-result-bindings branch October 26, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* needs rebase Conflicts with main, rebase needed perf Performance change shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants