Skip to content

perf(agc): free a registered shader's decoded program once it is planned - #5910

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
milescoviello:perf/registration-drop-decoded
Oct 15, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
milescoviello:perf/registration-drop-decoded

Conversation

@milescoviello

Copy link
Copy Markdown
Contributor

What

Borderlands 4 (PPSA01494) registers shaders in a burst about 190 s into a warm run, and its anonymous resident memory grows from about 1 GB to 8–9 GB within six seconds. A jemalloc heap profile taken in that burst shows 638 MiB held by RDNA programs that PlanRegistered decoded (RdnaInstructionDecoder::Decode ← PlanRegistered ← Driver::RegisterShader ← sceAgcCreateShader).

Since #2583 prepares registered shaders in the background, PlanRegistered decodes the program into the RegisteredPreparation it queues for the registration prepare pool. Only PlanRegistered reads that decoded program, for the s_swappc_b64 check; PrepareRegistered does not use it, and the preparation decodes the program again on its own (ShaderPreparationContext::AcquireFrontend). So every shader waiting in the pool's queue holds a decoded copy of its program until a worker has prepared it.

PlanRegistered decodes into a local again, as it did before #2583, and the queued preparation no longer carries it.

Tested

Ubuntu 24.04, GCC 13.3 (CI's compiler), Release with CI's configure flags, on main 84b01f9, then rebased onto main a453108 (which changes only libSceVoice since):

  • All 541 tests in libSceAgcDriver pass. agc_driver_stencil_ref_export is skipped because the device lacks VK_EXT_shader_stencil_export, and agc_host_import_sparse and agc_driver_sparse_dma_buf_import because /dev/udmabuf is not accessible here.

  • Borderlands 4 (PPSA01494), relinked with --to-intel and run on Linux (RTX 4070 Ti, NVIDIA 580.142) with a warm shader cache under a 15 GB memory cap, once per build under jemalloc 5.3 with sampled heap profiles (prof:true,lg_prof_interval:32,lg_prof_sample:19). The two runs differ only in libSceAgcDriver.prx. Live heap in the last profile before the run reached the cap (about 181 s):

    live heap, by allocating caller main this PR
    RDNA programs decoded by PlanRegistered 638 MiB none
    RDNA programs decoded by AcquireFrontend (preparations in progress) 42 MiB 45 MiB

    Most of the rest of the heap at that point is resource plans decoded from the shader cache (1.7–1.9 GB) and SPIR-V optimization in progress, which this change does not touch.

  • Without jemalloc the title still reaches the 15 GB cap in that burst, at 201–209 s, with and without this change: 3 of 3 runs with this PR, and 2 of 3 on main, where the third stopped earlier at a glibc heap check that this machine's memory produces with and without changes (see perf(agc): return freed memory when shader preparation drains #5777). So this change alone does not let it through on that budget.

  • tools/check_conventions.py --base upstream/main passes, and tools/pr_overlap.py --branch finds no overlap.

Not checked on Windows or on a console.

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
  • Unblocks: none
  • AI-assisted: yes

🤖 Generated with Claude Code

@CnRJay CnRJay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked: on main, RegisteredPreparation::decoded is only read by the UnresolvableSwappcTarget call in PlanRegistered, and the preparation decodes again in AcquireFrontend. So every queued shader held a decoded copy it never used, and decoding into a local keeps behaviour identical. Clean change with a clear heap profile behind it.

It should also help #5872, which reports the same registration burst pushing Demon's Souls out of memory.

@milescoviello

milescoviello commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

A second title: Forspoken (PPSA03984) on Linux (RTX 4070 Ti, 595.99.02, GCC 13.3, Release). The base is main fa539dd with #5937, #5975 and #5982 merged, since Forspoken stops earlier without them. Each run used a warm shader cache under a 15 GiB cap, and jemalloc heap profiles were taken every 2 GiB allocated (prof:true,lg_prof_interval:31,lg_prof_sample:19). This compares the last profile of each run:

live heap by first frame without this PR with it
ShaderRecompiler::DecodeRdnaProgram 1,208 MiB 20 MiB
total 12,524 MiB 11,949 MiB

The total barely moves because the run with this PR got further before the cap stopped it: its last profile came after 82 dumps instead of 61. Its resource plans (decodePlan, IrValue::AddArgument, ownPlanValues) grew from 3.1 to 4.0 GiB in that time.

AI-assisted: yes

@github-actions

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
#6178 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L727-728, L772

Since registered shaders are prepared in the background (boykopovar#2583),
PlanRegistered keeps the decoded RDNA program in the
RegisteredPreparation it queues for the preparation pool. Only
PlanRegistered reads it, for the s_swappc_b64 check, so each queued
shader holds its decoded program until a pool worker has prepared it.
During the registration burst of Borderlands 4 (PPSA01494) these
programs held 638 MiB.

Decode into a local again, as before boykopovar#2583.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@milescoviello
milescoviello force-pushed the perf/registration-drop-decoded branch from 01b4ca8 to 54f5c41 Compare October 15, 2026 02:57
@oneandonlydean
oneandonlydean merged commit 83fe634 into boykopovar:main Oct 15, 2026
8 checks passed
TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
Main brings the pipelined-draw stack (boykopovar#1193/boykopovar#1201/boykopovar#1202), boykopovar#6316 after boykopovar#6060, boykopovar#5893, boykopovar#5910 and depth/HTILE work (boykopovar#454, boykopovar#5496, boykopovar#5554, boykopovar#5524, boykopovar#5627, 626cff7). Our SharedSpirv / cold-SPIR-V storage is replaced by main's mapped cache entries (CreateFileMappingW on Windows), so cached variants' words are views into the cache files instead of heap copies; APS5_COLD_SPIRV, APS5_COLD_SPIRV_FILE and APS5_CACHE_SPIRV_BACKING are gone. Kept local: lane hang fixes, boykopovar#5815, boykopovar#6227, table stores and storage tables, option 1 substitutes (.slots), stencil remap and alias path (a written storage view our alias path serves seeds from the depth surface instead of retiring it), 10_11_11 blending next to main's reversed-order blending, VRAM share/spill/fallbacks, diagnostics. Versions: FormatVersion 43, RuntimeAbi 17, request serializer v31. Notes in work/sync-20261013/*-4.md.

AI-assisted: yes
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