Skip to content

fix(agc): keep fused geometry programs out of the caller's memory - #5573

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
fma-rc:fix/agc-fuse-halves-scratch
Oct 14, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
fma-rc:fix/agc-fuse-halves-scratch

Conversation

@fma-rc

@fma-rc fma-rc commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Refs #5099. Stray (PPSA02100) and Kena (PPSA01746) stop on current main with

AGC driver: guest memory is not readable at 0xdfac00000008 (range 0xdfac00000008+0x4)

The draw is a fused geometry shader. sceAgcFuseShaderHalves copied both halves' code, the fused header and its tables into the caller's memory and registered the program at that address. Stray fuses different halves into the same memory a few calls apart while draws that use the earlier result are still queued. The registry keeps one program per code address, so those draws were compiled from the later program, whose code reads a user-data word the bound layout does not have. With APS5_TRACE_SHADER_REGS=all one run shows two registrations at the same code address twelve log lines apart, with RSRC2 0x008b0018 and then 0x008b0010, and the failing draw carries the first value.

The export in the PS5 5.02 module writes only the register list into that memory: it copies the back half's header, memcpys num_sh_registers * 8 bytes, patches the checksum, RSRC1, RSRC2 and program address entries from the front half, and its size query returns num_sh_registers * 8 with alignment 4. No code goes there.

This PR keeps the fused code, header and tables in memory libSceAgc owns, one program per distinct pair of halves, registered once. The caller's memory receives the fused register list and the size query reports that list. Fusing the same halves again returns the same program; halves whose contents changed get a new one.

The driver matched a header against the registered one field by field, including the register list's address. A header whose register list is an identical copy elsewhere now matches as well, which is what a title passes after a fusion. Without that every run stops on rectangle ABI refers to a replaced shader header.

This changes where #124 put the fused program; its code layout and register merge are untouched. PR 4826 edits another function in the same file. #5384 works around the same stop by recording the unreadable root as poison; with this change the read does not happen, so the two do not depend on each other.

Limits. The fused list still appends the front half's own SH registers as #124 does, so it can be longer than the real one; a title that sizes the memory without the size query would be overrun by those entries (before this PR it was overrun by the whole program). Geometry halves without memory still throw, where the real function patches the back half's list in place. A fused result whose register list the title has overwritten no longer matches its header and throws refers to a replaced shader header; neither title does that.

Tested

Arch Linux, GCC 16.2.1, main d167000 with this PR.

agc_shader_fusion fails on main with fused size is not the fused register list; agc_driver_prepared_shaders and its _indirect, _uncached and _debug variants fail without the ShaderRegistry.cpp change with graphics ABI refers to a replaced shader header.

test NVIDIA RADV lavapipe
agc_shader_fusion pass pass pass
agc_driver_prepared_shaders pass pass pass
agc_driver_prepared_shaders_indirect pass pass pass
agc_driver_prepared_shaders_uncached pass pass pass
agc_driver_prepared_shaders_debug pass pass pass
agc_driver_prepared_shaders_debug_failure pass pass pass
agc_driver_prepared_shaders_deferred pass pass pass
agc_driver_prepared_shaders_deferred_undecodable pass pass pass
agc_driver_prepared_shaders_registered_workgroup pass pass pass
agc_driver_prepared_shaders_registered_workgroup_sync pass pass pass

Windows Server 2025, WinLibs GCC 15.2 (no GPU): build and ctest, 1103 tests, 0 failed.

Stray, runs of 120 s on the RTX 5080, alternating builds of main 30f9612 and that commit with this PR:

build runs stopped on the line above reached 120 s
main 6 2 (at 71 s and 77.5 s) 4
this PR 6 0 6

On main d167000 with this PR (the commit before a last change that leaves struct padding out of the cache key): Stray 4 of 4 reach 120 s, Kena 1 of 2; the other Kena run stops at 120 s on buffer descriptor uses an unsupported type, a stop Kena also has without this change. No run has BDA access failed or guest memory is not readable. On the older base Kena reached 120 s in 2 of 2 runs. I checked that the titles keep presenting; I did not compare pictures.

On Intel Arc (Meteor Lake, Mesa 26.2.3 ANV) ThatsLeo ran this head on Stray: the BDA access failed ... 0xdfac00000004 lines and the stop of presents after the calibration screen are gone, one run (#5099 (comment)).

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: PPSA02100 and PPSA01746 past the stop of Kena (PPSA01746): a draw's SRT walk reads through an unmapped pointer (0xdfac00000008) and aborts #5099
  • GPUs tested: NVIDIA RTX 5080 (615.78.08), AMD Radeon 610M (RADV), lavapipe, all on Arch Linux; Windows Server 2025 build and ctest without a GPU
  • AI-assisted: yes

sceAgcFuseShaderHalves copied both halves' code, the fused header and its
tables into the caller's memory and registered the program at that
address. PPSA02100 fuses different halves into the same memory a few
calls apart, while draws that use the earlier result are still queued.
The registry keeps one program per code address, so those draws were
compiled from the later program: its code read a user-data word the
bound layout does not have, and the device reported "BDA access failed"
for the mesh stage.

The PS5 5.02 module writes only the back half's register list into that
memory (sceAgcGetFusedShaderSize reports eight bytes per register,
alignment 4) and leaves the code where the halves are.

The fused code, header and tables now live in memory libSceAgc owns, one
program per distinct pair of halves, registered once. The caller's
memory receives the fused register list and the size query reports that
list. A second fusion of the same halves returns the same program; halves
whose contents changed get a new one.

The driver looked a header up by comparing its fields with the
registered header, including the register list's address. A header whose
register list is a copy elsewhere with the same contents now matches
too, which is what the title passes after a fusion.

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

The overwrite diagnosis is compelling, and the scratch-reuse regression covers the original failure mode. One lifecycle question: FusedPrograms retains a full key copy and fused storage for every distinct half pair until process shutdown. Do the title runs show that the number/total size of distinct fusions stays bounded during a longer session? If not, this changes reusable caller scratch into process-lifetime storage and could accumulate across dynamically generated shader pairs.

@MarcZX10

Copy link
Copy Markdown
Contributor

Linux validation on PR head d6f67d1be2ef7dc7cc0e9167cb7fc79197e381cc (GCC 12.2, CMake/Ninja Release, BUILD_TESTING=ON): built agc_shader_fusion_tests and ran ctest -R "^agc_shader_fusion$" (1/1 passed). Also built agc_driver_prepared_shaders_tests and ran ctest -R "^agc_driver_prepared_shaders" (9/9 passed). No failures. These focused Linux tests complement the reported title A/B; the open question in my review about process-lifetime growth of distinct FusedPrograms entries remains unanswered.

@fma-rc

fma-rc commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

I measured it. A local build of this head with a counter (one line per new entry and the running total of key plus storage bytes) on both titles:

title run fusion calls distinct programs bytes kept
Stray 420 s, 9,648 frames 3 2 12,610
Kena 124 s 2 2 12,610

Both fuse when a shader pair is first set up, a handful of times in a session. In Stray the second call was a hit on the first entry, and the last new entry appeared 70% of the way through the log, so the map follows the pairs the title has loaded.

What stays unbounded is a title that builds new halves at run time: each distinct pair would keep about 6 KB until exit. I have not seen one. I left eviction out because a draw queued earlier may still name an entry's code address, which is the failure this PR removes.

Kena's run ended on shader buffer exceeds descriptor range limit, a stop it also reaches without this change.

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

Thanks for measuring the lifetime retention. The two title runs each retain about 12.6 KB across two distinct fused programs, with later calls mostly reusing existing pairs. That addresses my lifecycle question for these observed workloads. The dynamic-pair case remains process-lifetime storage, and the PR explains why eviction could invalidate code still referenced by queued draws. I previously ran the focused fusion and prepared-shader suites on this exact head; I see no blocker from the retention data.

@github-actions github-actions Bot added the agc libSceAgc* label Oct 14, 2026
@xXPBSXx

xXPBSXx commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Windows test of #5573: Windows 10 (19045), MinGW-w64 GCC 15.2, Release, ninja -j 4, ctest called directly; ctest ran without -V, so the logs of the passing runs do not show which GPU, if any, the tests used (not verified). Tree: main 095a653c8 + merge of PR head d6f67d1be2ef7dc7cc0e9167cb7fc79197e381cc (local merge 7fa9258eb, no conflict). Logs: rtx5573_*_N.log, rtx5573_result.txt.
10 ctest targets, 3 runs each, exit code 0 in 30 of 30 runs: agc_driver_prepared_shaders, agc_driver_prepared_shaders_debug, agc_driver_prepared_shaders_debug_failure, agc_driver_prepared_shaders_deferred, agc_driver_prepared_shaders_deferred_undecodable, agc_driver_prepared_shaders_indirect, agc_driver_prepared_shaders_registered_workgroup, agc_driver_prepared_shaders_registered_workgroup_sync, agc_driver_prepared_shaders_uncached, agc_shader_fusion.
Not tested: that the new or changed tests fail without the change (no baseline run), other ctest targets, the AMD GPU unless named above, Linux, a title run.

AI-assisted (Claude Code).

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.

4 participants