Skip to content

fix(shader): link a table call whose descriptor load sits before its loop - #7413

Open
thuanquang wants to merge 2 commits into
boykopovar:mainfrom
thuanquang:fix/table-call-hoisted-descriptor
Open

thuanquang wants to merge 2 commits into
boykopovar:mainfrom
thuanquang:fix/table-call-hoisted-descriptor

Conversation

@thuanquang

@thuanquang thuanquang commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What

Little Nightmares Enhanced Edition (PPSA10737) stops before its first frame in 3 of 3 runs on main (@zaryabzip, discussion #6275):

[gpu] driver failure: compute shader 0x2010fa0000: shader 0x2010fa0000 stage 0: ShaderRecompiler::Recompile: computed/data-dependent s_swappc_b64 call target at program counter 0x00001034 is not statically resolvable

The shader calls a function from a table once per lane, in a waterfall loop. Its agc_shader_replay --asm listing in #6275 shows the shape:

  • the table's V# is loaded once, with s_load_dwordx4 s[96:99], s[0:1], 0x10 at 0x0c08, well before the loop;
  • inside the loop, each lane's record is loaded with s_buffer_load_dwordx4 s[16:19], s[96:99], vcc_lo;
  • the call is s_swappc_b64 at 0x1034.

FindTableCalls (#4706) links a table call only when the s_load_dwordx4 that defines the V# is in the call's own block: findLastWriter stops at the first branch, and sameBlock rejects any label in between. The loop header is always such a label, so the call is never linked, and the shader is compiled with the raw computed call, which throws.

The same-block rule is still tried first, so every call main links is linked the same way. When it fails, the descriptor is now looked for with a reaching-definition walk (reachingWriter). It goes back from the record load over every predecessor of each instruction: the previous instruction unless it is s_branch, s_endpgm, s_code_end or s_setpc_b64, every direct branch to it, and every s_setpc_b64 whose target resolveSetpcTarget resolves to it (the long-branch forms the control flow analysis already follows). Each path stops at the first write of any of the four descriptor registers. The search fails, and the call stays unlinked as on main, if:

  • a path reaches the program's first instruction without a write;
  • two different writes reach the load;
  • a path crosses s_call_b64 or a statically resolved call, whose callee is not modelled;
  • a path reaches the entry of an in-program function (an s_call_b64 target). The callers' paths are not followed, so this fails even when the instruction before the entry falls through into it;
  • a path reaches an instruction that nothing modelled jumps or falls into.

The paths may run through any s_swappc_b64 that is not resolved statically, including the table call itself through the loop's back edge. Its callee is assumed to keep the descriptor registers. If it doesn't, nothing wrong runs: the linked compare chain compares the target each iteration actually loads with the address of each linked copy. A miss takes the UnlinkedCall fault (#4706), so the worst case is that fault, never a wrong callee. Every other check is unchanged:

  • the writer must be an s_load_dwordx4 from an unchanged user-data pointer or, since feat(agc): link calls through the AGC FS table #6160, from a constant pointer that two s_mov_b32 set in the writer's block;
  • the record load, its copies and the s_lshl_b32 must be in the call's block.

Overlap: #6160, since merged, changed the same line of FindTableCalls (it added a reader argument to the findLastWriter call this replaces). This branch is rebased onto it, and the two are independent:

The reporter wrote that #6160 does not fix this stop.

Tested

Windows 11 26200, AMD Radeon RX 6700 XT (gfx1031), driver 32.0.21045.5002, WinLibs GCC 15.2.0 r7, Release, BUILD_TESTING=ON, ANYPS5_REQUIRE_VULKAN=1:

  • scalar_call has a new CheckHoistedTableCalls, built from the Little Nightmares Enhanced Edition (PPSA10737) #6275 listing, run for wave32 and wave64.
    • These must link (userData s0, descriptor offset 0x10, stride 16) and translate: the V# loaded inside the loop (the existing Ratchet shape), loaded before a branch and the loop (the title's shape), rewritten after the loop, and loaded before the loop when the loop skips a null target with s_cbranch_scc0 past the call.
    • The V# loaded before the loop through a constant pointer (s_mov_b32 of 0xe0040000 and 15 into s[106:107], feat(agc): link calls through the AGC FS table #6160's FS-table form) must link with that constant pointer and descriptor offset 0.
    • These must stay unlinked and still throw "not statically resolvable":
      • a second writer of s96 on one branch arm;
      • the V# loaded on one arm only;
      • s97 rewritten after the call inside the loop;
      • an s_call_b64 to an in-program function on the branch arm;
      • a second V# load (offset 0x20) on a path that rejoins before the loop through an s_getpc_b64 / s_sub_u32 / s_setpc_b64 long branch.
    • Three more programs:
      • a function entered only through s_call_b64 with two paths to the record load, one through a second V# load, must stay unlinked;
      • a V# load, an in-program s_call_b64 and the call in one block must link as on main;
      • a function entry (an s_call_b64 target) that the code above it falls into, with the V# loaded in that code, must stay unlinked.
    • With main's GraphBuilder.cpp the hoisted shapes find no table call, so the test fails.
  • agc_driver_compute_table_calls has a second caller whose V# is loaded before a conditional branch. On this GPU, records 0 and 1 must call their callees (16 and 33 in every lane) in wave32 and wave64.
    • With main's GraphBuilder.cpp the dispatch stops with the title's message: computed/data-dependent s_swappc_b64 call target at program counter 0x00000038 is not statically resolvable.
  • Rebased onto main c34e9d855 after feat(agc): link calls through the AGC FS table #6160, which lets the same-block search cross a forward branch past the call; the same-block check keeps that, and reachingWriter follows every predecessor already. scalar_call, agc_driver_compute_table_calls and agc_driver_compute_fs_table_calls pass 3 of 3 on the rebased head. With GraphBuilder.cpp at c34e9d855, the first two fail as on main (computed/data-dependent s_swappc_b64 call target at program counter 0x00000038 is not statically resolvable) and feat(agc): link calls through the AGC FS table #6160's test passes.
  • Follow-up commit cc69016, after an independent review of the walk. Each new check fails without its half of the change and passes with it:
    • Without the s_setpc_b64 edges, the long-branch shape links the 0x10 table, and the linked program translates (wave32, local probe). Lanes that took the long branch would then reach their targets only through the UnlinkedCall fault. With the edges, the search finds both loads and leaves the call unlinked.
    • Without the entry check, FindTableCalls returns the fall-through program's call. GraphBuilder then rejects the linked program (unclosable scalar call/return pairing at program counter 0x00000004: control flow enters the call region from program counter 0x0000000c). With the check, the error stays main's not statically resolvable. So this check changes no compiled shader; it keeps the driver from reading a table through a V# that the call path never loads.
    • scalar_call, agc_driver_compute_table_calls and agc_driver_compute_fs_table_calls pass 3 of 3 on cc69016.
  • Full ctest --parallel 4 --timeout 120 on main bb2624a53 with this change: 1469 tests. The failures all happen without it too: agc_driver_address_passes (broken on Windows GPUs by perf(agc): settle Windows imports under 1 MiB without waiting #6678, fixed on main by fix(agc): keep a settled import compared while a storage image overlaps it #7476), agc_driver_lds_mixed_width_atomics (fix(shader): release the LDS lock with an exchange and drop fences inside it #6976), agc_driver_lds_lock_budget (AGC: AMD device-backed LDS lock-budget case returns half of expected sum #7344) and agc_driver_layered_volume_vertex_no_gpl (AMD Windows: identical AGC executables invert GPL pass/fail results after relocation #4768).
  • python tools/check_conventions.py --base origin/main with this title and body passes.
  • Runs by others:
    • @klosixgaming-arch ran Little Nightmares Enhanced Edition (PPSA10737 01.004.000) at cc69016 on an RTX 5090. No [gpu] driver failure for a computed s_swappc_b64 target. The title then stopped in the same compute shader 0x2010fa0000 on a separate gap, GetImageResource dword 0 is not a valid runtime value (reported on feat(shader): read samplers through nested and pointer-keyed descriptor tables #7395). With a local fix for that, the shader compiles with the call at 0x1034 linked by this PR. The title then stops in shader 0x200c340000, whose table calls take their V# from 4-dword slices of an s_load_dwordx8. That shape is not part of this PR (see below).
    • @wachi35676: RTX 5070 Ti and AMD RDNA2 iGPU (1002:13c0, 32.0.21042.62), at d622a55 and cc69016. scalar_call and agc_driver_compute_table_calls pass on both, and fail with the title's message with GraphBuilder.cpp from the base. 728 of 728 pass in the agc_|shader|recompiler|scalar_call set on NVIDIA. On AMD only main's three known failures fail.
    • @klosixgaming-arch: RTX 5090, cc69016 on a373e85. Both tests pass, and both fail on main with only this PR's test files.
    • @savvaniss: Intel UHD 770 and AMD RX 9070 XT, cc69016 merged into 0737757. Both tests pass 3 of 3, and fail on main (lane 0 is 3405691582, expected 16).
  • Not tested: the call shapes of the title's other deferred shaders (the reporter counted 346 with computed s_swappc calls).

Next stops seen in the title, not addressed here:

  • table calls whose V# is a 4-dword slice of an s_load_dwordx8/x16, which FindTableCalls does not accept;
  • with those accepted (tried locally by @klosixgaming-arch), shader 0x200c340000's fourth table call lands out of the 16-bit branch range. LinkCalls appends each call's dispatch chain and its own callee copies in call order. The copies can't be shared between calls, because a callee region reached from two s_call_b64 sites is rejected (is shared with another call). So linking all six calls of that shader needs far calls from the chains, which have to keep SCC for the callee.

Checklist

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 18, 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 4448 docs/dev/debt/libSceAgcDriver.md
PR 4449 docs/dev/debt/libSceAgcDriver.md
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5499 docs/dev/debt/libSceAgcDriver.md
PR 5925 docs/dev/debt/libSceAgcDriver.md
PR 6044 core/libs/tests/ScalarCall.cpp
PR 6146 docs/dev/debt/libSceAgcDriver.md
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6218 docs/dev/debt/libSceAgcDriver.md
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6612 docs/dev/debt/libSceAgcDriver.md
PR 6636 docs/dev/debt/libSceAgcDriver.md
PR 6683 docs/dev/debt/libSceAgcDriver.md
PR 6727 docs/dev/debt/libSceAgcDriver.md
PR 6741 docs/dev/debt/libSceAgcDriver.md
PR 6762 docs/dev/debt/libSceAgcDriver.md
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6868 docs/dev/debt/libSceAgcDriver.md
PR 6941 docs/dev/debt/libSceAgcDriver.md
PR 6946 docs/dev/debt/libSceAgcDriver.md
PR 6947 docs/dev/debt/libSceAgcDriver.md
PR 6958 docs/dev/debt/libSceAgcDriver.md
PR 6966 docs/dev/debt/libSceAgcDriver.md
PR 6977 core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7008 docs/dev/debt/libSceAgcDriver.md
PR 7254 docs/dev/debt/libSceAgcDriver.md
PR 7256 docs/dev/debt/libSceAgcDriver.md
PR 7258 docs/dev/debt/libSceAgcDriver.md
PR 7332 docs/dev/debt/libSceAgcDriver.md
PR 7390 docs/dev/debt/libSceAgcDriver.md
PR 7432 docs/dev/debt/libSceAgcDriver.md
PR 7446 docs/dev/debt/libSceAgcDriver.md
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7505 docs/dev/debt/libSceAgcDriver.md
PR 7604 docs/dev/debt/libSceAgcDriver.md
PR 7642 docs/dev/debt/libSceAgcDriver.md
PR 7683 docs/dev/debt/libSceAgcDriver.md
PR 7729 docs/dev/debt/libSceAgcDriver.md
PR 7800 docs/dev/debt/libSceAgcDriver.md

@github-actions github-actions Bot added shader core/shader docs agc libSceAgc* labels Oct 18, 2026
@screamm

screamm commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Second-GPU run on head d622a55dc, Windows 11 Pro 26220, NVIDIA GeForce RTX 4070 (596.21), WinLibs MinGW-w64 GCC 15.2, Release. Stacked on main a62c724c3 together with #7381 (f405b7cae); the two merge cleanly and share no file. Targeted build (relinker, libs and the shader and agc_driver_* image/sample/shader/table test executables), not a full build.

  • scalar_call and agc_driver_compute_table_calls pass 3 of 3. The latter ran on the RTX 4070 (no skip) and covers the hoisted caller for records 0 and 1 in wave32 and wave64.
  • Negative control: GraphBuilder.cpp set back to main with the tests kept. agc_driver_compute_table_calls fails with [gpu] driver failure: ... computed/data-dependent s_swappc_b64 call target at program counter 0x00000038 is not statically resolvable and the first record's callee through a descriptor loaded before a branch (wave64): lane 0 is 3405691582, expected 16. scalar_call fails too. Restored, both pass.
  • ctest -j1 --timeout 300 -R '^(agc_driver_(shader|table|descriptor|sample|image|volume|3d)|shader_|agc_shader_|scalar_call)': 81 of 81 pass.
  • check_conventions.py --base origin/main passes and git diff --check is clean.
  • No regression elsewhere: PPSA99005 (OpenGL Showcase) and PPSA01153 (OliSe Player), 60 s each, main (3e3e8b905) against the stack: no stop line, no driver failure and the same dump content on both. These titles do not reach a computed call, so this says nothing about the change itself. PPSA10737 is not available to me, so the title is not tested.

Not checked: PPSA10737, the full ^agc_driver_ pass and the non-GPU suite, SPIR-V validation with spirv-val, AMD/Intel, Linux.

Written with Claude Code assistance (validation by a subagent, reviewed in this session).

@hugodearaujo

Copy link
Copy Markdown
Contributor

EA SPORTS FC 25 (PPSA20049) needs this too. On main e5f18ea with #6343 and #7606 it stops 230 s after start on compute shader 0x1184ca4400 (computed/data-dependent s_swappc_b64 call target at program counter 0x000004c4 is not statically resolvable); its table V# is loaded at 0xa0, before the waterfall loop. With this PR's head (d622a55) merged on top, it reaches its main menu and stays there until the 620 s run ends (#7607).

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
…loop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thuanquang
thuanquang force-pushed the fix/table-call-hoisted-descriptor branch from d622a55 to 499e2db Compare October 18, 2026 17:58
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
reachingWriter followed only fall-through and direct branches, so a V#
load on a path that reaches the loop through an s_setpc_b64 jump the
control flow analysis resolves was not seen, and another load could be
taken as the only writer. The walk now follows those jumps. It also
fails at s_call_b64 targets, whose callers it does not follow, instead
of walking from a function entry into the code above it.

Co-Authored-By: Claude <noreply@anthropic.com>
@wachi35676

Copy link
Copy Markdown
Contributor

Tested on Windows 11 (GCC 15.2.0, Release) on an RTX 5070 Ti (617.42) and an AMD Radeon iGPU, RDNA2 (1002:13c0, 32.0.21042.62, ANYPS5_GPU=AMD), at d622a55dc and again at cc6901685.

  • scalar_call and agc_driver_compute_table_calls pass on both GPUs.
  • With GraphBuilder.cpp set back to the base, both fail on both GPUs with computed/data-dependent s_swappc_b64 call target at program counter 0x00000038 is not statically resolvable.
  • The full agc_|shader|recompiler|scalar_call set at cc6901685 passes 728/728 on NVIDIA. On AMD it has only the three failures main has there (lds_mixed_width_atomics, lds_lock_budget, layered_volume_vertex_no_gpl).

AI-assisted: yes (Claude Code); results above were run on this machine.

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

Tested on Windows 11, MinGW-w64 GCC 15.2.0, NVIDIA GeForce RTX 5090 (driver 617.42), Ryzen 9 9950X3D. Head cc6901685 merged onto current main (a373e85); clean merge.

  • agc_driver_compute_table_calls and scalar_call pass with the change.
  • On main with only this PR's test files both fail, so the new cases do need the fix.
  • I read reachingWriter: the old same-block search is still tried first, so the calls main links today are linked the same way. The new walk goes back over predecessors and gives up (returns false, so the call stays unlinked as before) if it reaches a call or s_swappc_b64, or finds two different writers of the descriptor registers. That is the conservative direction. Reading the diff, I did not find a path where it links a call it should not (I did not fuzz it).

Not tested: Little Nightmares Enhanced Edition (PPSA10737); I have no dump, so the title claim is the author's.

AI-assisted: yes. Written with AI assistance and reviewed by me.

@klosixgaming-arch

Copy link
Copy Markdown
Contributor

Tested on a PC with a real title, Little Nightmares Enhanced Edition (PPSA10737, version 01.004.000), running the PR head (cc69016) as built, relinked and launched on Windows (RTX 5090).

The relinked game started and ran for about 2 minutes 46 s before it stopped. The log shows the shaders that call s_swappc_b64 through a computed target being deferred to first use ("could not be prepared at registration ... preparing it when it is used"), and the run went on to create compute pipelines (for example 170624 SPIR-V words in 1089 ms) and to record draws and dispatches. There was no [gpu] driver failure for a computed s_swappc_b64 target. I did not run main on this dump in this session, so I am not claiming a before and after measurement here.

The game then stopped on a different shader: compute shader 0x2010fa0000, stage 0, ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value (the address chain goes through nested Phi nodes, Ballot and ReadFirstLane), then SIGABRT, exit 0xC0000409. That looks like a separate recompiler gap and not something this PR should fix. Only 3 presents were logged before it, so no frame was shown. The ctest target I ran (agc_write_tracking) passed.

AI-assisted: yes. Written with AI assistance and reviewed by me.

@klosixgaming-arch

Copy link
Copy Markdown
Contributor

Follow-up on Little Nightmares Enhanced Edition (PPSA10737, version 01.004.000) on Windows 11 / RTX 5090 at cc6901685:

With a local fix in TryMakeIndirectColumn for the GetImageResource dword 0 stop on compute shader 0x2010fa0000 (where the s_swappc_b64 at 0x1034 is linked by this PR), 0x2010fa0000 compiles ([dispatch] created a pipeline for 414421 SPIR-V words in 26173 ms (program 0x2010fa0000)).

The title then advances to compute shader 0x200c340000 (2,572 words, firstUser=0, userCount=14, wave=32) and stops at:

[videoout] presentation failed: compute shader 0x200c340000: shader 0x200c340000 stage 0: ShaderRecompiler::Recompile: computed/data-dependent s_swappc_b64 call target at program counter 0x000008b4 is not statically resolvable

Inspecting 0x200c340000 shows 6 table calls (0x0460, 0x08b4, 0x0c7c, 0x0d2c, 0x16a0, 0x1990):

  • 0x0460 and 0x0c7c read from s[4:7] (loaded by s_load_dwordx4 s[4:7], s[0:1], 0x0 at 0x0204) and are linked by FindTableCalls on cc6901685.
  • 0x08b4, 0x0d2c, 0x16a0, and 0x1990 read from s[88:91] and s[92:95], which are loaded before the loops by a single s_load_dwordx8 s[88:95], s[0:1], 0x10 at 0x04c0 (0xf40c1600 0xfa000010), so table.op != RdnaOpcode::SLoadDwordx4 skips them.
  • Accepting 4-dword-aligned V# subranges of SLoadDwordx8/SLoadDwordx16 in FindTableCalls links 0x0460, 0x08b4, and 0x0c7c, and then stops in LinkCalls on the 4th call (0x0d2c) with compute shader 0x200c340000: the table callees of the call at program counter 0x00000d2c lands out of the 16-bit branch range.

AI-assisted: yes. Written with AI assistance and reviewed by me.

@savvaniss

Copy link
Copy Markdown
Contributor

Intel and AMD Windows data: both tests fail on main and pass with this PR, on an Intel UHD 770 and an AMD RX 9070 XT.

Windows 11 Pro 26300, WinLibs GCC 15.2.0 r7, Release, every execution test built, ANYPS5_NO_SHADER_CACHE=1, serial CTest. Intel UHD Graphics 770 (8086:a780), driver 32.0.101.7088. AMD Radeon RX 9070 XT (1002:7550), driver 32.0.31041.1004, with VkCache emptied first. This head cc6901685 merged into main 07377573f. "Without" means this PR's test files on main 07377573f, with none of its other changes.

Tests, ×3 UHD 770 RX 9070 XT
This head: agc_driver_compute_table_calls, scalar_call 2 of 2 pass 2 of 2 pass
Without both fail; compute_table_calls: the first record's callee through a descriptor loaded before a branch (wave64): lane 0 is 3405691582, expected 16 same

I also ran full CTest with this PR and the 13 other open AGC and shader PRs I tested in the same batch merged together (#7668, #7659, #7504, #7432, #7426, #7480, #7465, #7466, #7464, #7449, #7489, #7673, #7560). Neither GPU showed a failure beyond main's own: the five image-table tests (#6647) and agc_driver_sample_multisample_target (#6957) on the UHD 770, and agc_driver_layered_volume_vertex_no_gpl (#4768) on the RX 9070 XT.

AI-assisted: yes (Claude Code)

@thuanquang

Copy link
Copy Markdown
Contributor Author

Thanks @klosixgaming-arch for the title runs and for tracking the next two stops. @wachi35676 and @savvaniss, thanks for the runs too. The description now lists all of them.

On shader 0x200c340000:

  • Accepting a 4-dword-aligned V# slice of s_load_dwordx8/x16 in FindTableCalls is small: the V# is read at memoryOffset + 16 * slice.
  • The branch-range stop that follows comes from LinkCalls appending each table call's dispatch chain and its own callee copies in call order, so later chains drift past the 16-bit s_branch reach. The copies can't be shared between calls: GraphBuilder rejects a callee region reached from two s_call_b64 sites (is shared with another call, pinned in ScalarCall).
  • So linking all six calls there needs far calls from the chains. resolveSetpcTarget already resolves s_getpc_b64, s_add_u32 imm, s_swappc_b64 on the link pair, which the call overwrites anyway. The catch is SCC: the chain hands the caller's SCC to the callee unchanged, and the s_add_u32 must sit right before the s_swappc_b64. Restoring SCC at the callee would need two entries into one copy, which the call model rejects. So it needs a way to carry SCC across a far call, or two copies per callee.

That's a follow-up after this PR and #7395, not something to add here. Do you plan to open the x8/x16 part, since you have it locally? If not, I can open it with a ScalarCall case for the slice offsets.

This branch has not been deployed

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

Labels

agc libSceAgc* docs shader core/shader unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants