Skip to content

feat(shader): resolve user-data s_swappc_b64 call targets - #2302

Closed
santhoshkumar-dev wants to merge 1 commit into
boykopovar:mainfrom
santhoshkumar-dev:codex/user-data-swappc
Closed

santhoshkumar-dev wants to merge 1 commit into
boykopovar:mainfrom
santhoshkumar-dev:codex/user-data-swappc

Conversation

@santhoshkumar-dev

Copy link
Copy Markdown
Contributor

What

Addresses #1817; related reports: #2062 and #2207.

Compute shaders can copy a function pointer out of user-data SGPRs and call it with s_swappc_b64. Registration has the shader's code and ABI but no invocation user data, so the existing static call analysis rejects these programs before their target is available. Preparing one program per original caller also cannot represent dispatches that select different callees.

Registration now defers only compute programs whose unresolved targets have a proven user-data origin. The analysis follows s_mov_b32/s_mov_b64 definitions, rejects branches that can bypass a definition, and stops at control transfers and earlier calls. An untouched user-data pair remains valid across loops. A pair written elsewhere in the caller is accepted only before its first write and without a back edge across the call. Every captured callee must preserve the source pairs used by this proof. Arbitrary GPU-data-dependent targets still throw.

At dispatch, the existing ShaderMemory capture reader synchronizes and reads each callee from guest memory, bounds its size, and decodes it through its final s_setpc_b64. The caller's instructions and PCs stay intact. Each call site gets its own callee clone and explicit target/return binding, so one link pair can serve several calls, the same callee can be invoked at several sites, and every return reaches that site's continuation. Relative branches inside a callee retain their offsets. This avoids a dynamic return selector and changes no instruction translation semantics. The existing PC + 4 link write and s_setpc_b64 return follow AMD's RDNA2 ISA, sections 3.6.3 and 12.3, opcodes 32/33. Native ISA/PS5 equivalence has not been measured.

The source, prepared-interface and disk keys include all combined caller/callee bytes plus each call's PC mapping, target address and user-data provenance. Source lookup still compares the full code on hash matches; disk entries verify the complete key. Prepared handles and captures reject bindings that no longer match their invocation's function-pointer words. Callee reads participate in dispatch-cache validation and are recorded as code reads, so the flat-resource-data fast path cannot mask a changed instruction. Targets aliasing immutable registration snapshots are rejected. Request serialization version 14 and agc_shader_replay --dis --cfg preserve the combined program and call bindings.

Remaining unsupported cases throw: non-compute driver integration, arithmetic-derived targets, copy chains that fail the conservative control-flow proof, nested calls/cycles, PC-relative callees, escaping link values, and callees without one final return to the correct link pair. Limits are 64 captured call sites, 64 KiB per callee and 1 MiB combined code. Existing static/direct and fetch-call validation remains in place.

The overlap check was run against current open PRs, with its local query limit also extended to include all open PRs. No other open PR implements this callee-resolution capability. There are mechanical overlaps with #1836/#2257 (registration deferral), #1902/#1928/#2261 (prepared shader lookup), #1491/#1776 (frontend), #1492 (serialization version), #1547 (cache review list), and #2178 (technical debt). This PR contains no prerequisite commits and does not depend on those changes; their overlapping hunks need reconciliation if merged first.

Tested

All new fixtures are synthetic; no retail title or commercial shader file was used. No compatibility claim is made for Ghost of Yōtei, Astro Bot, Bloodlines 2, or any other retail title.

user_data_swappc exercises move-chain provenance, a direct user-data call sharing its link pair, four call sites/two pointers, separate continuations, repeated callee cloning, serialization and source/disk key sensitivity. Negative controls retain the original unbound-graph rejection, corrupt a return mapping, introduce GPU lane data into the target, bypass a defining move, reuse a clobbered pointer in a loop, and exercise invalid/null/unmapped/misaligned targets, malformed/truncated code, recursion/nested calls, unsupported PC-relative code, source-pair clobbers, link escapes, address overflow and size/call-count bounds.

agc_driver_user_data_swappc registers without function-pointer values, resolves at dispatch, and checks observable callee additions and caller additions after every return. It covers wave32/wave64, four shared-link calls, repeated calls inside a loop, two user-data pairs, target changes, and edits to both callees at unchanged addresses. It also checks deterministic guest-memory failures, stale prepared-source rejection, capture of return bytes, registration-snapshot alias rejection, and exclusion of code reads from flat-data masking.

Windows x64, WinLibs GCC 15.2.0 r7 (x86_64-ucrt-posix-seh), Release, AMD Radeon RX 6700 XT. Configuration used BUILD_TESTING=ON and ANYPS5_ENABLE_SPIRV_TOOLS=ON; builds used --parallel 2 and CMAKE_BUILD_PARALLEL_LEVEL=2.

  • cmake --build build-gcc15 --parallel 2 — complete build passed.
  • ctest --test-dir build-gcc15 --output-on-failure -R 'swappc|prepared|shader_disk_cache|structurizer|control_flow|shader_memory|cache_control' — all 18 passed (5.96 s), including all existing direct/fetch SWAPPC, graph, disk-cache and prepared-shader scenarios.
  • The new dispatch executable also passed with disk caching enabled in a fresh ANYPS5_SHADER_CACHE_DIR, with ANYPS5_NO_SHADER_CACHE unset; it wrote 19 artifact files while exercising target/code changes.
  • ctest --test-dir build-gcc15 --output-on-failure --parallel 2 — complete 512-test suite: 508 passed, one skipped, three failed (113.73 s). This includes the complete shader execution suite. The skip is the existing no-Vulkan WQHD detection case.
  • All three failures reproduced with identical messages on an unchanged checkout of main d70b89989473ba1f6ae13e44e079e67f1f8a44b0, built with the same GCC 15.2/SPIR-V configuration and GPU: agc_driver_pixel_interlock (wave64 BDA-fault case counts 1 instead of 0), agc_driver_sdwa_float_selectors (lane 2 yields 0xba instead of 0), and agc_driver_sdwa_ldexp_f16 (lane 2 yields 0xfff0 instead of 0). Their assertions and instruction lowering were not changed. windows_dependency_diagnostics passed on both supported-toolchain checkouts.
  • An earlier exploratory GCC 16.1 run passed the 14 initially focused tests and disk-enabled dispatch checks; its full suite had the same three GPU failures plus a Windows dependency-diagnostics timeout. Final validation uses the repository's supported GCC 15.2 toolchain.
  • python tools/check_conventions.py --base origin/main and git diff --check origin/main...HEAD passed. tools/pr_overlap.py was run, including a supplementary all-open-PR scan rather than only its default 200-PR window.

Linux/macOS and native PS5 execution were not tested locally.

Checklist

  • Based on current main; no other open PR implements the same capability
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unsupported paths throw; no silent stubs added
  • No notes, investigation files or images added to the repository
  • No new third-party code
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes — OpenAI Codex/Astra

@github-actions

github-actions Bot commented Oct 9, 2026 •

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
#1491 core/shader/recompiler/Recompiler.cpp L104-105
#1776 core/shader/recompiler/Recompiler.cpp L97
#1836 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L178-180, L219-224, L368
core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp L5-6, L26
core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp L892
#1902 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L178-180, L219-224
#1928 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L204-205
#2178 docs/dev/TechnicalDebt.md L378
#2257 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L178-180, L219-224, L368
core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp L5-6, L26
core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp L892
#2261 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L178-180, L219-224
#2313 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L178-181, L219-224
#2327 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp L2-3, L368-370
#2381 docs/dev/TechnicalDebt.md L369, L378-379

@github-actions github-actions Bot added build shader core/shader docs agc libSceAgc* labels Oct 9, 2026
@gjonn

gjonn commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

GT7 performance gains observed on our local Windows/NVIDIA build:

  • Main menu: about 2.3 FPS previously → about 6.8–7 FPS now, after accumulated local changes.
  • First logged presentation after the perf(recompiler): compute post-dominators visiting successors first #2105 optimization: about 36 seconds, compared with roughly 131–161 seconds in earlier runs. Boot also felt substantially faster.
  • Offline shader control-flow processing: 31.27 seconds → 2.11 seconds across 2,035 decoded graphs (about 14.8x faster).

These are local before/after observations, not measured gains from #2302, which we have not tested yet.

@snoozedDev

Copy link
Copy Markdown
Contributor

Tested on Linux with a retail title. Setup: Omarchy (kernel 7.2.5), GCC 16.2.1, RTX 3090, NVIDIA 610.57.04 (subgroup 32), PR head cfa024b on main d70b899, configure -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON, full build and --target libs.

[libc] unhandled exception 'St16invalid_argument' thrown from ...
terminate called after throwing an instance of 'std::invalid_argument'
  what():  computed/data-dependent s_swappc_b64 call target at program counter 0x0000009c is not statically resolvable

The throw comes out of AnalyzeUserDataCalls at registration (I wrapped that call locally to print the program). The program's first call, at 0x48, is the shape your proof accepts: the pair is copied from s[0:1] at 0x2c/0x30 and s0 is then reused by s_load_dword at 0x34 before the call. The second call, at 0x9c, is the one rejected. Disassembly up to it (decoder output; family=1 is SOP1, opcode=0x03/0x04 are s_mov_b32/s_mov_b64, 0x21 is s_swappc_b64, family=12 is SMEM):

0x00000004: s_mov_b32  vcc_lo, s16
0x00000008: s_mov_b64  s[16:17], s[2:3]
0x0000000c: s_mov_b64  s[84:85], s[14:15]      <- reads the link pair before the first call
0x00000010: v_lshl_add_u32 v1, vcc_lo, 6, v0
0x00000018-0x00000028: v_mov v4..v8, v5
0x0000002c: s_mov_b32  s14, s0
0x00000030: s_mov_b32  s15, s1
0x00000034: s_load_dword s0, s[8:9]            <- s0 reused after the copy
0x0000003c: s_waitcnt
0x00000040: v_... v2, s3, s0
0x00000048: s_swappc_b64 s[14:15], s[14:15]    <- accepted
0x0000004c-0x0000005c: v_mov v5, v6, v4, v0, v7
0x00000060: s_mov_b32  s14, s4
0x00000064: s_mov_b32  s15, s5
0x00000068: s_mov_b32  s16, s6
0x0000006c: s_mov_b32  s17, s7
0x00000070: s_load_dword s86, s[8:9]
0x00000078-0x0000007c: v_mov v3, v8
0x00000080: s_waitcnt
0x00000084: v_... v24, s86, s1
0x0000008c: v_lshlrev_b32 v28, s84, v1
0x00000090: s_... s81, s11, 0x00280000
0x00000098: s_mov_b32  s80, s10
0x0000009c: s_swappc_b64 s[14:15], s[14:15]    <- rejected

s4 and s5 are never written in the program (userCount 16, base s0). Nothing between 0x60 and 0x9c writes s14 or s15. The only difference from the first call I can see is that it is the second call through the same link pair, so the previous writer of s[14:15] in program order, before the copies at 0x60/0x64, is the call at 0x48 itself. If the proof stops at the earlier call when it walks back from 0x9c, that would explain it; the copies sit after that call, so the earlier call should not count as a definition of the target.

For scale: dumping every program main + #1836 defers in this title (96 of 839 registered, 27 distinct PCs from 0x40 to 0x204) shows these recurring shapes, all in SDK vertex-pulling compute programs:

  1. four calls through one link pair s[14:15], the first via s[4:5] and three via s[8:9], each site preceded by its own s_mov_b32 pair (the Ghost of Yōtei: compute shader calls a user-data function pointer with s_swappc_b64 (not statically resolvable) #1817 program);
  2. the pointer copied from s[0:1] or s[2:3], and that register then reused by s_load_dword before the call (above);
  3. s_mov_b64 s[84:85], s[14:15] or s_mov_b64 s[4:5], s[14:15] before the first call, i.e. a read of the link pair before any call writes it;
  4. s_cmp/s_cbranch_scc/s_trap guards between the copy and the call (first call at 0x124 in that family), so forward branches whose source and target both lie between the copy and the call.

I have an independent implementation of the same idea on snoozedDev:feat/shader-user-data-call (one commit, not opened as a PR since yours is first) whose analysis accepts all four with these rules: last writers of the target pair in program order; user-data source unwritten up to the copy; no branch from outside entering the stretch between copy and call; calls resolved at dispatch may share a link pair when their regions are disjoint, with one appended copy per site; reads of the pair before the first call sharing it are allowed. With it, 0 of the 839 registered programs are left unresolved; happy to run your next revision on the title.

Not reached on either implementation: a dispatch of one of these programs. On the 3090 the title stops first at #997's v_readlane_b32 of lane 63 in a wave64 hull program (NVIDIA-only, see #2340); on this machine's RADV iGPU it stops at #1921's continue-construct validation, which #1964 does not cover. So the SDK callee bodies are still unverified against either subroutine decoder.

AI-assisted: yes (Claude Code); reviewed by me.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: resolving the targets at dispatch puts per-invocation data into the prepared artifact, which goes against how #639 compiles at registration, and the title still stops right after it. #1836 is the route for these shaders: they are deferred and still throw until the calls are actually translated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants