Skip to content

perf(agc): release unused registered shader resource plans - #6178

Merged
adriadam10 merged 4 commits into
boykopovar:mainfrom
MarcZX10:codex/release-undrawn-resource-plans-6139
Oct 18, 2026
Merged

adriadam10 merged 4 commits into
boykopovar:mainfrom
MarcZX10:codex/release-undrawn-resource-plans-6139

Conversation

@MarcZX10

@MarcZX10 MarcZX10 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

Fixes the resource-plan retention reported in #6139. After registered shader preparation completes, release its cached plan when it has not been captured for runtime use and disk-plan caching is enabled. The plan is stored under the existing request cache key, so the first runtime use can reload it. Runtime capture marks the plan as used, and each capture retains its own shared plan so it remains valid across a reload.

When a released plan misses the disk cache, the recompiler must rebuild the resource plan. If the source already has a compiled variant for the exact binding layout, this rebuild no longer leaves a full IR program resident. If no matching variant exists, the IR remains available for compiling that layout.

Tested

  • Rebased on current main at a08de6827f217115078660b9f47201edae59668b.
  • Release configure with pinned bundled dependencies and BUILD_TESTING=ON.
  • Built agc_shader_disk_cache_tests and ran ctest --test-dir build --output-on-failure -R '^agc_shader_disk_cache$': passed (1/1).
  • Red/green regression: removing the new retention guard makes the test fail with a plan reload retained full IR; with the guard it passes.
  • python3 tools/check_conventions.py --base upstream/main --head HEAD and git diff --check upstream/main...HEAD pass.
  • Linux x86-64, GCC 12.2 Release. To build this target with GCC 12, I temporarily added the required template disambiguator in an unrelated existing source file, then removed it; it is not part of this pull request. No GPU, Windows, or console test was run.

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 or are documented as silent stubs
  • No notes, investigation files, or images added to the repository
  • New third-party code is a submodule built from source
  • Generic behavior, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: no GPU test; the regression exercises CPU-side plan caching
  • AI-assisted: yes

@MarcZX10
MarcZX10 force-pushed the codex/release-undrawn-resource-plans-6139 branch from 7224df1 to 9f00bfa Compare October 15, 2026 00:35
@github-actions github-actions Bot added build shader core/shader agc libSceAgc* labels Oct 15, 2026
@fma-rc

fma-rc commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

@MarcZX100 I had a local fix for the same holder from #6139 before I saw this PR, so I won't open a second one. It takes a different route, and the numbers may help you decide.

Mine never loads the plan at registration when the variant comes from the disk cache. The first capture loads or builds it (ensurePlan under the entry's lock), and a cold run builds it at registration as main does today. On Demon's Souls (PPSA01341), warm cache, 240 s on main ab9a476, plan loads went from 3971 to 141 and peak RSS from 15.3 GB to 13.5 GB. Cold was 9.7 GB vs 9.6 GB. The branch is fix/agc-lazy-resource-plans on my fork (b77092a), with a red/green mode in agc_shader_disk_cache.

One thing I noticed reading this PR: StorePlan only enqueues the write, and LoadPlan reads the file alone. A plan released before its write lands can't be reloaded, so the first capture rebuilds the front end and stores it again. The test calls ShaderDiskCache::Flush() before the reload, so it wouldn't see this. I'm running Demon's Souls cold and warm on this PR now and will add the plan counters and RSS here.

@fdfawef324

Copy link
Copy Markdown
Contributor

Confirmed a retained-full-IR consequence of the fallback @fma-rc identified, on head 9f00bfa3d8945d128a711f0b0df0240f4e549c74 against its exact base.

Using a small synthetic buffer shader and unmodified production recompiler, I ran PrepareShader -> ReleaseUndrawnResourcePlan -> prepared Capture -> Materialize -> PrepareShader -> Flush. When the released plan's file is missing, corrupt, or unwritable, getSource rebuilds both source->plan and source->program. The existing compiled artifact satisfies materialization and the repeated prepare, so nothing consumes or resets the rebuilt full IR.

Plan storage This head retains full IR Base retains full IR
Persisted, flushed before release No No
Entry removed after preserving it Yes No
Entry's magic corrupted after preserving it Yes No
Cache enabled, writes fail Yes No

The retained fixture has two blocks/seven instructions alongside its eight-node plan, surviving every phase above. All sixteen paired materializations preserve output and prepared artifact identity. Cache-off, disk-plans-off, active-capture and failed-capture controls pass; the existing --resource-plan-codec suite also passes.

The fallback needs to release the rebuilt full IR when an existing artifact serves the request. These are CPU-only lifetime measurements; no whole-title memory or FPS effect is claimed.

AI-assisted: yes (Codex)

@fdfawef324

Copy link
Copy Markdown
Contributor

Prepared a small follow-up for this head: f1b42dae46, available to cherry-pick. It avoids keeping newly rebuilt full IR when an existing artifact serves the request, including compatible relocated push layouts. It preserves IR waiting for a new layout and validates the prepared source handle before suppressing retention.

All eight lifetime scenarios now pass, including missing/corrupt/unwritable plan storage; their materialized outputs and source/artifact identities match the base. The new CPU regression fails on this PR's original head and passes with the fix; the existing resource-plan codec suite passes. Selective controls also catch dropping pending new-layout IR, omitting the relocated-layout hint, and skipping its validation.

Test host: NVIDIA GeForce RTX 3060 12 GB, driver 591.86 (32.0.15.9186), Vulkan 1.4.325, Windows Server 2025 10.0.26100. This validation is CPU-only, with no GPU/title test or whole-title RAM/FPS claim. No duplicate PR opened.

AI-assisted: yes

Co-Authored-By: Codex noreply@openai.com

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@MarcZX10
MarcZX10 force-pushed the codex/release-undrawn-resource-plans-6139 branch 2 times, most recently from bb7e524 to bb4a178 Compare October 15, 2026 09:55
@MarcZX10

Copy link
Copy Markdown
Contributor Author

Thanks for identifying and validating the cache-miss retention case. I rebased #6178 onto current main and pushed a fix in bb4a178f. When rebuilding a missing plan, it now drops the rebuilt IR if a compiled variant already covers that exact binding layout; it still retains IR when the requested layout needs compiling.

I added a red/green regression to agc_shader_disk_cache_tests: it fails on the rebased PR without the guard and passes with it. The updated PR description records the Linux test run and the temporary GCC 12 compatibility workaround used only in an unrelated source file. No GPU or title result is claimed.

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

github-actions Bot commented Oct 15, 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 1341 core/shader/recompiler/CMakeLists.txt
PR 3247 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 4072 core/shader/recompiler/Recompiler.cpp
PR 4650 core/shader/recompiler/Recompiler.cpp
PR 5021 core/shader/recompiler/Recompiler.cpp
PR 5115 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 5500 core/shader/recompiler/Recompiler.cpp
PR 5609 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp
core/shader/recompiler/Recompiler.cpp
PR 5925 core/shader/recompiler/CMakeLists.txt
PR 6177 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6280 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6486 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 6503 core/shader/recompiler/Recompiler.cpp
PR 6539 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp
core/shader/recompiler/Recompiler.cpp
PR 6867 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6879 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 6941 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 7201 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 7223 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/Recompiler.cpp
PR 7381 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp
core/shader/recompiler/Recompiler.cpp

@fdfawef324

Copy link
Copy Markdown
Contributor

@MarcZX100 Thanks, c00b0dfb9 fixes the exact-layout case: the eight lifetime scenarios from my earlier comment all pass on it when the draw layout equals the registration layout. One case is still open, a prepared capture with a relocated push layout.

MatchesPreparedShader lets a prepared artifact serve a request whose push offset and size differ from the artifact's. materializeDrawStage builds {0, 0, pushOffset, 128 - pushOffset}, while registration uses {0, 0, 0, 128}, so without graphics pipeline libraries a fragment stage after a vertex stage with push data gets a relocated layout. If that capture reloads a released plan whose entry is missing, corrupt or unwritable, getSource finds no variant with sameLayout and keeps the rebuilt source->program. The prepared artifact then serves the draw, so nothing consumes or resets that IR.

Results on c00b0dfb9 (CPU-only, synthetic compute fixture; retention probed with ResourcePrograms() when preparing a new layout afterwards):

Capture persisted missing corrupt writes fail cache off disk plans off active capture failed capture
Exact layout ok ok ok ok ok ok ok ok
Relocated, PreparedShaderInvocation::Capture ok IR kept IR kept IR kept ok ok ok ok
Relocated, CaptureResources(request, runtime, handle) ok IR kept IR kept IR kept ok ok ok ok

Minimal red test, placed after your regression in verifyPlanRoundTrip. On c00b0dfb9, --resource-plan-codec stops with a plan reload retained full IR after the prepared artifact served a relocated push layout.

Test diff
@@ -1893,6 +1893,31 @@ void verifyPlanRoundTrip() {
     static_cast<void>(PrepareShader(secondLayout, &preparation));
     require(preparation.ResourcePrograms() == 1u, "a plan reload retained full IR after an artifact for that layout was already available");
 
+    auto relocatedFixture = fixtures.front();
+    relocatedFixture.waveSize = 32u;
+    relocatedFixture.code.insert(relocatedFixture.code.begin(), 0xbf800005u);
+    const auto relocatedRequest = relocatedFixture.Request(true);
+    const auto relocatedHandle = PrepareShader(relocatedRequest);
+    static_cast<void>(GetResourcePlan(relocatedRequest));
+    ShaderDiskCache::Flush();
+    std::vector<std::byte> relocatedKey;
+    ShaderDiskCache::BuildPlanKey(relocatedRequest, 32u, relocatedKey);
+    const auto relocatedPath = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(relocatedKey);
+    std::filesystem::rename(relocatedPath, relocatedPath.string() + ".reload-preserved");
+    ReleaseUndrawnResourcePlan(*relocatedHandle);
+    auto drawn = relocatedRequest;
+    drawn.layout.pushConstantOffsetBytes = 16u;
+    drawn.layout.pushConstantSizeBytes = 112u;
+    const auto relocatedInvocation = PreparedShaderInvocation::TryCreate(drawn, relocatedHandle);
+    require(relocatedInvocation.has_value(), "the prepared artifact refused a relocated push layout");
+    RequestMemoryView drawnMemory(drawn.context.memory);
+    static_cast<void>(relocatedInvocation->Materialize(*relocatedInvocation->Capture(drawnMemory.MakeRuntime(drawn.context.userData, drawn.shader.codeAddress))));
+    auto relocatedProbe = relocatedRequest;
+    relocatedProbe.layout.pushConstantSizeBytes = 0u;
+    ShaderPreparationContext relocatedPreparation;
+    static_cast<void>(PrepareShader(relocatedProbe, &relocatedPreparation));
+    require(relocatedPreparation.ResourcePrograms() == 1u, "a plan reload retained full IR after the prepared artifact served a relocated push layout");
+
     RequestMemoryView registeredMemory(registeredRequest.context.memory);
     const auto runtimeCapture = CaptureResources(registeredRequest, registeredMemory.MakeRuntime(registeredRequest.context.userData, registeredRequest.shader.codeAddress), *registeredHandle);
     const auto beforeKeep = ShaderDiskCache::Totals();

A fix: pass the prepared handle from both capture paths through acquirePlan into getSource, and skip keeping the rebuilt IR when that handle's artifact serves the request. The handle is checked against the source and request before the entry changes. On top of your guard this gives exactly the Recompiler.cpp from f1b42da. Both commits are on fdfawef324:claude/pr6178-relocated-layout (aaaebc3 test, a376d01 fix), ready to cherry-pick. With the fix, all 24 runs above, --resource-plan-codec, the full agc_shader_disk_cache_tests run and the conventions check pass.

There was no GPU or title run. The cost is memory held by the retained IR; output doesn't change.

AI-assisted: yes

@milescoviello

Copy link
Copy Markdown
Contributor

A title measurement, since the description has none: Crimson Desert (PPSA25286, 01.000.568). It compiles about 12,000 to 13,000 shaders in its first five minutes and was one of the titles growing host memory in #6139.

Setup: Linux, RTX 4070 Ti, NVIDIA 595.99. Both builds are main 5d9dcc4 + #6612 + #6613, which Crimson Desert needs to get past its first stops; the second adds c00b0dfb9. Cold shader cache, 300 s with scripted pad input, a 14 GiB memory cap, and /proc/<pid>/status sampled every second. One run each.

without #6178 with #6178
shader cache misses (compiled) at 300 s 13,324 12,029
resource plans: disk hits / misses 0 / 9,391 116 / 8,154
RssAnon at 120 s 2,770 MB 2,328 MB
RssAnon at 240 s 5,482 MB 4,272 MB
RssAnon at 298 s 7,727 MB 4,720 MB
RssAnon per compiled shader at 298 s 0.58 MB 0.39 MB
peak VmHWM 14,781 MB 14,213 MB

VmRSS and VmHWM include the mmapped shader cache files and memfd guest memory, so they sit near the cap either way. RssAnon is the part that grew without bound in my earlier runs, which ended at the 14 and 16 GiB caps. With the PR it grows about a third slower per compiled shader, and the 116 plan hits look like the reloads after release. Neither run drew a frame within the 300 s (the title is still compiling), so this shows the memory only, not rendering.

AI-assisted: yes

fdfawef324 and others added 2 commits October 16, 2026 15:24
A prepared artifact also serves draws whose push constants start at another
offset. When such a capture reloads a released plan that is missing from the
disk cache, the rebuilt resource program must not stay on the source entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepared captures pass their source handle to the plan reload. When the handle
matches the request, its artifact serves the draw, so the rebuilt resource
program is not kept even if the push layout differs from the artifact's.
A handle that does not match the source is rejected before the entry changes.
Same change as f1b42da on top of c00b0df.

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

Copy link
Copy Markdown
Contributor Author

I reviewed the proposed relocated-layout regression and fix, then carried the two commits onto this PR while preserving fdfawef324 as the author. Locally, agc_shader_disk_cache_tests builds and passes on Linux/GCC 12.2, including the new reload path. The build needed a temporary GCC 12 compatibility edit in unrelated SrtDescriptorEvaluation.cpp; I reverted it before pushing. No GPU/title test was run.

@adriadam10 adriadam10 self-assigned this Oct 18, 2026
@adriadam10
adriadam10 merged commit fb9a1eb into boykopovar:main Oct 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants