Skip to content

fix(shader): plan a table T# that several image operations share - #5550

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
milescoviello:fix/shader-shared-table-image
Oct 14, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
milescoviello:fix/shader-shared-table-image

Conversation

@milescoviello

@milescoviello milescoviello commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

Crimson Desert (PPSA25286) stops at start-up while it prepares one of its vertex programs:

Shader 0x20ac0f4100 could not be prepared after registration: shader 0x20ac0f4100 stage 1: ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x0000043c(GetBufferResource(...), ShiftLeftLogical32(ReadFirstLane(SelectU32(...)), Immediate))

The program reads a T# from a descriptor table at a wave-uniform key and uses it twice:

0x434  v_readfirstlane_b32   vcc_lo, v4
0x438  s_lshl_b32            vcc_lo, vcc_lo, 5
0x43c  s_buffer_load_dwordx8 s[0:7], s[16:19], vcc_lo
0x448  image_get_resinfo     v1, v1, s[0:7]
0x47c  image_sample_lz       v6, v[0:1], s[0:7], s[24:27]

TryMakeTableImage already plans this shape (32-byte entries, a uniform key) as a bindless image table. It refused this one because it requires the eight scalar reads to have no user other than the one GetImageResource it is planning. SplitDescriptorPhis runs first. When it finds a Phi in the chain behind a T#, here in the lane condition of the v_cndmask_b32 that builds the key, it gives each image operation its own copy of the GetImageResource over the same eight reads. The query and the sample therefore have two handles, each read has two users, and the table is refused. GetHandle then rejects the scalar reads with the message above.

A read now passes the check when every user is a GetImageResource built from exactly these eight reads. Each copy is planned as the same table. Both get the same descriptor source, so the query and the sample share one table image and one set of slots.

agc_shader_bindless gains a case for this: a loop whose counter is part of the key, with image_get_resinfo and image_sample_lz on the same table T#. The loop Phi makes the split give the two operations separate copies, as in the title. The case checks that the plan has one table source and one image for both operations, that the capture binds the table slots, and that the program compiles to valid SPIR-V.

Instructions of the new case, after the four s_load_dwordx4 the existing wholeCode starts with:

s_mov_b32 s40, 0
v_mov_b32 v2, 0
loop:
v_readfirstlane_b32 s16, v0
s_add_u32 s16, s16, s40
s_lshl_b32 s16, s16, 5
s_buffer_load_dwordx8 s[20:27], s[4:7], s16
image_get_resinfo v[4:7], v2, s[20:27] dmask:0xf dim:2D
image_sample_lz v[8:11], v[0:1], s[20:27], s[8:11] dmask:0xf dim:2D
s_add_u32 s40, s40, 1
s_cmp_lt_u32 s40, 2
s_cbranch_scc1 loop
buffer_store_dword v8, off, s[28:31], 0
buffer_store_dword v4, off, s[28:31], 0 offset:4
s_endpgm

#5188 also targets GetImageResource dword 0 is not a valid runtime value, for a T# read at an offset the CPU walk can evaluate. With #5188 applied, the title still stops on this shader, because its key is not one the walk can evaluate. #5089 changes the same file for per-lane T#s in waterfall loops, and #1184, #757, #758 and #4475 extend the table planning. tools/pr_overlap.py --branch finds no overlap with any of them.

Tested

Ubuntu 24.04, GCC 13.3 (CI's compiler), Release with CI's configure flags, on main 7a0c838:

  • With main's ResourceTracker.cpp and the new case, agc_shader_bindless fails with the title's message, GetImageResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x00000034(...). With the change it passes.
  • All 500 tests in libSceAgcDriver pass on an RTX 4070 Ti (NVIDIA 580.142), the shader and driver tests included. agc_driver_stencil_ref_export is skipped, as on main, because the device lacks VK_EXT_shader_stencil_export.
  • The title's recompile request, replayed with agc_shader_replay, fails resource analysis with the message above on main. With the change, --prepare produces 36,023 SPIR-V words.
  • Crimson Desert (PPSA25286), relinked with --to-intel and run on Linux. Both runs include fix(libkernel): fail sceKernelStat with EFAULT for a null buffer #5464, fix(http2): allow completion waits without an event queue #3925 and fix(pad): accept vibration mode 5 #5523, which get the title past its earlier stops, and differ only in ResourceTracker.cpp:
    • Without the change it stops 7 s in on this shader (Shader 0x20ac0f4100 could not be prepared after registration).
    • With the change no shader fails to prepare. It runs to 18 s and stops on the next unsupported feature, VGT_SHADER_STAGES_EN=0x60212d: combined tessellation and geometry is unsupported by the reference path.
  • A MinGW-w64 GCC 13 -fsyntax-only -Wall -Wextra compile of both changed files has no errors. Its warnings, -Wclass-memaccess from IrValue.hpp and -Wmissing-field-initializers in the test file, are the same on main.
  • tools/check_conventions.py --base upstream/main passes.
  • @xXPBSXx ran it on Windows: Windows 10 (19045), MinGW-w64 GCC 15.2, Release, this PR merged onto main 34355eeea. agc_shader_bindless and agc_shader_memory pass in 3 runs each; that run had no baseline without the change.

Not checked 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

milescoviello and others added 2 commits October 13, 2026 23:28
TryMakeTableImage planned a T# read from a bindless image table only when
its eight scalar reads had no user other than that one GetImageResource.
Descriptor-phi splitting gives each image operation its own copy of the
T# over the same reads, so a table entry that is both queried and sampled
was refused, and GetHandle then rejected the scalar reads.

Accept reads whose every user is a GetImageResource built from exactly
those eight reads; each copy is planned as the same table.

Crimson Desert (PPSA25286) stops on such a shader at start-up:
image_get_resinfo and image_sample_lz on one s_buffer_load_dwordx8 T#.

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

github-actions Bot commented Oct 14, 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
#5620 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L926

@xXPBSXx

xXPBSXx commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Windows test of #5550: 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 34355eeea + merge of PR head cd1757e03659f4a363e1b73a2d8a3711808e084b (local merge 3d6a86e14, no conflict). Logs: rtx5550_*_N.log, rtx5550_result.txt.
2 ctest targets, 3 runs each, exit code 0 in 6 of 6 runs: agc_shader_bindless, agc_shader_memory.
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).

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks, @xXPBSXx. I've added the Windows run to the description.

AI-assisted: yes

@simonbronk

Copy link
Copy Markdown
Contributor

Title evidence: Onimusha: Way of the Sword (PPSA27836 01.000.001, RE Engine), Windows 11 Pro 26200, RTX 5070. On main 0b97f8b plus the start-up fixes listed in #5769, it stops preparing pixel programs that read their T# from 48-byte records at +16 (s_buffer_load_dwordx8 at key * 0x30), the key loaded through a readfirstlane inside a bit-mask loop:

GetImageResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x0000391c(..., IMul32(ReadConstBuffer@pc=0x00002f88(..., IMul32(ReadFirstLane(...), Immediate)), Immediate))

A later program reads the same table T# for two image operations. With #5620 and #5550 both merged, both pass and the title stops next at static bindless image capacity exceeded (#5720). Not run with only one of the two.

AI-assisted: yes (Claude Code).

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks for the Onimusha run. The later program, with one table T# used by two image operations, is the shape this PR handles (the copies of GetImageResource that SplitDescriptorPhis makes over the same eight reads). The 48-byte records at +16 behind a bit-mask loop sound like #5620's part.

AI-assisted: yes

@oneandonlydean
oneandonlydean merged commit b5c593a into boykopovar:main Oct 14, 2026
10 checks passed
Celegans12 added a commit to Celegans12/AnyPS5 that referenced this pull request Oct 14, 2026
Materialize runtime-indexed image and sampler records into descriptor
tables while keeping descriptor selection in the shader. Report invalid
active entries through the existing deferred fault buffer.

Bind fragment tables in the fragment set when pipeline libraries split it,
and reserve a physical descriptor for empty tables. Reject unsupported
sampler, border, cube and anisotropic pairings per entry. Keep one column
per heap access shape, one T# shared by several image operations included
(milescoviello's boykopovar#5550 case), and narrow it to the union of its keys'
domains when every key has one (SleglJan's key-domain changes). Plan
columns read at (key << n) & mask over their whole domain (tibzejoker's
fix). Read records in runs through SleglJan's bulk reader, and reject
entries whose surface has no mapped byte, as Tamalero proposed. Store
table columns in disk plans; update the runtime ABI and disk format. Bound
table-sized entries in the resource cache (65,536 table elements) and the
dispatch cache (256 MiB of captured words), evicting LRU.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants