You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Marvel's Guardians of the Galaxy (PPSA01748): compute shader 0x3b91c3e00 indexes an SRT pointer table with ReadFirstLane of a bit index taken from an ImageRead mask (GetImageResource dword 0 is not a valid runtime value) #7362
Guardians now stops on a shader shape that no open PR covers. A compute shader loops over the set bits of a mask (part of the mask comes from an ImageRead), scalarizes each bit index with ReadFirstLane, and uses it to index a table of T#s behind a 64-bit pointer from the SRT. The recompiler refuses the T# load at registration. The title dispatches the shader after the main menu, and the abort follows.
This is shape 2 from my #2715 comment. Shape 1 (loop-counter index) was fixed by #757. Shape 2 is now the first stop. #6232, #6606 and #6417 don't change it. #7223 handles a different shape (record → pointer → V# → T#) and can't reach this load, so I have not run it here.
Details and logs
Title: Marvel's Guardians of the Galaxy (PPSA01748 v01.006.000) Where it stops: about 2–3 min in, after the main menu (top option), on main 5d9dcc42d plus #6232 and #6606, which are needed to get this far. Without #6606 it stops earlier, on #6526.
CachyOS, RTX 4090 (NVIDIA proprietary), Release. 5 of 5 runs: 2 on that stack and 3 with #6417 added, all at the same shader and pc.
Key:ReadFirstLane(SelectU32(cond, a, b)), a waterfall over the lanes. In the visible arm, a is FindILsb32 of a mask that the loop updates with BitwiseXor32 (clear the lowest set bit). The mask starts from a CompositeExtractU32x4(ImageRead, …) component. In that arm, the key is a bit index in 0–31. The other arm goes through Phis and a bit-cast FPMax32, so I can't say the key is always bounded.
The loop condition (LogicalAnd with FPOrdLessThan32) is also per lane, so which records one dispatch reads depends on the image contents.
I haven't read the T# table at run time or measured how many records it holds, so I don't know whether a 32-record bound would cover every key. I haven't attached the RecompileRequest dumps either, because they contain the game's shader code.
Related: #2715 (shape 2), #6191 (the Guardians half there is #6232's two-pointer chain, fixed), #757, #5089 (ReadLane-copy waterfalls; this chain has 0 ReadLane copies).
Tested and written by Claude (AI assistant) on a contributor's machine.
This appears distinct from #7223: the key here can come from ImageRead data inside the loop, while #7223 handles a pointer/record chain the planner can resolve. Before choosing a fix, could you share (without title shader data) the SRT entry’s declared/allocation bounds and whether the table length is available at runtime? The visible FindILsb32 arm gives keys 0–31, but the alternate Phi/FPMax arm is not proven bounded. I would avoid adding a 32-entry assumption from that one arm. If the table can be shorter or the other arm can exceed it, the intended behavior for an out-of-range guest index also needs to be explicit.
I am investigating the IR value chain for this compute shader (0x3b91c3e00) and the loop structure in Optimization/src/SrtWalker/SrtRuntimeValidator.cpp. Looking closely at how the waterfall ReadFirstLane(SelectU32(...)) indexes the SRT pointer table: specifically checking whether the planner can track the SRT slot allocation bounds or table descriptor size for the base pointer to enforce safe runtime bounds for the dynamic key rather than assuming a fixed 32-entry width across both arms. Working on a clean recompiler planner reproduction test fixture in core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp.
Thanks for the update. I see #7388 is now open against this path, with a bounded ReadFirstLane(SelectU32(...)) planner reproduction. I will not open a competing implementation. The title run’s SRT allocation bounds or runtime table length would still help confirm that the inferred key domain matches the real descriptor, if you can share that without shader data.
@MarcZX100 @pushkarreddyy here are the table bounds for the pc=0x4380 load, read from guest memory at the stop (Guardians, main0bc3003f8 + #6606 + #7388, #6270 reverted for #7678). Addresses are masked; offsets, sizes and contents are real.
Short answer: the table holds 16 T#s (512 bytes), and a different object starts right after it. No length is stored next to the pointer, so 16 is implied by the layout only. The FindILsb32 arm allows keys 0–31, and a key of 16 or more would read the neighbouring V#s and pointers instead of a T#.
Where the table pointer comes from. The chain's ReadConst(GetSrtResource) words (SRT slots 120/121, flat offset 0x78/0x79) aren't plain SRT values. They resolve through two loads: a user-data V# → ReadConstBuffer@pc=0x4c returns a pointer P → LoadAddressU32@pc=0x41d0 reads the qword at P + 0x10, which is the table base T.
What's around it:
Address
Contents
T + 0x000 … 0x1e0 (16 × 32 B)
16 T#s, all identical at this moment (2D image, type 0xa in dword 3)
T + 0x200
two buffer V#s (strides 0x20 and 0x10)
T + 0x220
one more T#
T + 0x240 = P
an array of 8 qword pointers: +0x00 → another single T#, +0x08 → the V# pair at T + 0x200, +0x10 → T, +0x18 → the T# at T + 0x220, then four more
after that
float constants (1.0, 1.1, 2.0, …), then zeros
So P is a descriptor-set list (pointers only, no counts), and the 16-entry table is one of the sets it points to. The key shift is consistent with a 32-byte stride: the 16 entries repeat exactly at 32 bytes.
This is one run, captured at the dispatch that stops. I can't evaluate the per-lane key on the CPU, so I can't say which indices the shader actually reaches. The data only shows that the backing allocation has 16 valid T#s.
How it was captured
The shader fails to prepare at registration, where its user data is still zero, and the dispatch then rethrows that stored failure. So a local-only hook re-ran resource planning once with the dispatch's request (live user data) and evaluated the refused chain against guest memory. The table and its neighbours were then read from the run's core at the same point. None of this is meant for upstream.
Tested and written by Claude (AI assistant) on a contributor's machine.
@Tamalero Thank you for this breakdown and the guest memory layout capture! This confirms the memory structure and explains the refusal.
Key Takeaways from the Memory Layout:
Descriptor Set Array (P) & Table Base (T):
P (at T + 0x240) acts as a descriptor-set pointer list where P + 0x10 links back to T.
The 32-byte element stride is confirmed by ShiftLeftLogical32(..., 5) and the 16 * 32 B repeating T# layout from T + 0x000 to T + 0x1e0.
The contiguous valid T# domain is strictly 16 entries ([0..15]), with heterogeneous objects (V# buffer descriptors at +0x200 and the pointer array P at +0x240) residing immediately adjacent in the same page allocation.
Why 0x3b91c3e00 is Refused:
The key is computed from ShiftLeftLogical32(ReadFirstLane(SelectU32(FindILsb32(CompositeExtractU32x4(ImageRead, ...)), ...)), 5).
Because ImageRead data is not compile-time constant or clean memory, possibleU32Bits treats the unconstrained bit search input as having up to 32 bits, giving FindILsb32 an upper bound of 31 (maxKey = 31).
A 32-entry key domain requires 1024 bytes (0x400), which extends across the +0x200 boundary into the buffer V#s and pointer table P. Because those subsequent dwords do not match valid T# descriptor layouts, table validation flags the out-of-domain entries and refuses the planning.
For this Guardians compute pattern, we are investigating:
Tracing the ImageRead extraction channel or preceding format constraints to check if the active mask is logically bounded to 16 bits.
For pointer-chain tables lacking explicit length headers, evaluating whether table snapshotting can cap the bound at the first non-T# boundary (the +0x200 V# discontinuity) when the pointer chain discovers the contiguous set.
Activity
MarcZX10 commentedon Oct 18, 2026
This appears distinct from #7223: the key here can come from
ImageReaddata inside the loop, while #7223 handles a pointer/record chain the planner can resolve. Before choosing a fix, could you share (without title shader data) the SRT entry’s declared/allocation bounds and whether the table length is available at runtime? The visibleFindILsb32arm gives keys 0–31, but the alternate Phi/FPMax arm is not proven bounded. I would avoid adding a 32-entry assumption from that one arm. If the table can be shorter or the other arm can exceed it, the intended behavior for an out-of-range guest index also needs to be explicit.pushkarreddyy commentedon Oct 18, 2026
I am investigating the IR value chain for this compute shader (
0x3b91c3e00) and the loop structure inOptimization/src/SrtWalker/SrtRuntimeValidator.cpp. Looking closely at how the waterfallReadFirstLane(SelectU32(...))indexes the SRT pointer table: specifically checking whether the planner can track the SRT slot allocation bounds or table descriptor size for the base pointer to enforce safe runtime bounds for the dynamic key rather than assuming a fixed 32-entry width across both arms. Working on a clean recompiler planner reproduction test fixture incore/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp.MarcZX10 commentedon Oct 18, 2026
Thanks for the update. I see #7388 is now open against this path, with a bounded
ReadFirstLane(SelectU32(...))planner reproduction. I will not open a competing implementation. The title run’s SRT allocation bounds or runtime table length would still help confirm that the inferred key domain matches the real descriptor, if you can share that without shader data.Tamalero commentedon Oct 18, 2026
@MarcZX100 @pushkarreddyy here are the table bounds for the
pc=0x4380load, read from guest memory at the stop (Guardians,main0bc3003f8+ #6606 + #7388, #6270 reverted for #7678). Addresses are masked; offsets, sizes and contents are real.Short answer: the table holds 16 T#s (512 bytes), and a different object starts right after it. No length is stored next to the pointer, so 16 is implied by the layout only. The
FindILsb32arm allows keys 0–31, and a key of 16 or more would read the neighbouring V#s and pointers instead of a T#.Where the table pointer comes from. The chain's
ReadConst(GetSrtResource)words (SRT slots 120/121, flat offset0x78/0x79) aren't plain SRT values. They resolve through two loads: a user-data V# →ReadConstBuffer@pc=0x4creturns a pointerP→LoadAddressU32@pc=0x41d0reads the qword atP + 0x10, which is the table baseT.What's around it:
T + 0x000 … 0x1e0(16 × 32 B)0xain dword 3)T + 0x2000x20and0x10)T + 0x220T + 0x240=P+0x00→ another single T#,+0x08→ the V# pair atT + 0x200,+0x10→T,+0x18→ the T# atT + 0x220, then four moreSo
Pis a descriptor-set list (pointers only, no counts), and the 16-entry table is one of the sets it points to. The key shift is consistent with a 32-byte stride: the 16 entries repeat exactly at 32 bytes.This is one run, captured at the dispatch that stops. I can't evaluate the per-lane key on the CPU, so I can't say which indices the shader actually reaches. The data only shows that the backing allocation has 16 valid T#s.
How it was captured
The shader fails to prepare at registration, where its user data is still zero, and the dispatch then rethrows that stored failure. So a local-only hook re-ran resource planning once with the dispatch's request (live user data) and evaluated the refused chain against guest memory. The table and its neighbours were then read from the run's core at the same point. None of this is meant for upstream.
Tested and written by Claude (AI assistant) on a contributor's machine.
pushkarreddyy commentedon Oct 19, 2026
@Tamalero Thank you for this breakdown and the guest memory layout capture! This confirms the memory structure and explains the refusal.
Key Takeaways from the Memory Layout:
Descriptor Set Array (
P) & Table Base (T):P(atT + 0x240) acts as a descriptor-set pointer list whereP + 0x10links back toT.ShiftLeftLogical32(..., 5)and the16 * 32 Brepeating T# layout fromT + 0x000toT + 0x1e0.[0..15]), with heterogeneous objects (V#buffer descriptors at+0x200and the pointer arrayPat+0x240) residing immediately adjacent in the same page allocation.Why
0x3b91c3e00is Refused:ShiftLeftLogical32(ReadFirstLane(SelectU32(FindILsb32(CompositeExtractU32x4(ImageRead, ...)), ...)), 5).ImageReaddata is not compile-time constant or clean memory,possibleU32Bitstreats the unconstrained bit search input as having up to 32 bits, givingFindILsb32an upper bound of 31 (maxKey = 31).0x400), which extends across the+0x200boundary into the buffer V#s and pointer tableP. Because those subsequent dwords do not match valid T# descriptor layouts, table validation flags the out-of-domain entries and refuses the planning.Follow-Up Direction:
ReadFirstLane/SelectU32and validating conjunctions against the active lane mask.ImageReadextraction channel or preceding format constraints to check if the active mask is logically bounded to 16 bits.+0x200V# discontinuity) when the pointer chain discovers the contiguous set.