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
Stray (PPSA02100): a pixel shader reads a 2D image descriptor as a buffer ("buffer descriptor uses an unsupported type") #5879
Stray (PPSA02100, Unreal Engine 4) on Windows 11, RTX 5070, MinGW GCC 15, Release, main066b58fa1, relinked with --windows. It boots, shows its brightness calibration screen, and accepts it. While the first level loads (black screen with the loading icon), it stops:
terminate called after throwing an instance of 'std::runtime_error'
what(): buffer descriptor uses an unsupported type
A local print at the throw in materializeTables (ResourceMaterializer.cpp) showed:
Word 3's top nibble is 0x9, the TYPE of a 2D image resource (T#), so the slot the pixel shader reads as a buffer holds an image descriptor. A few runs before this one, the same throw also showed up as an apparent hang: the terminating thread sat in ucrtbase!abort under LibcException::DefaultTerminate and the crash report was never written. That happened in 1 of 4 runs.
I haven't found which draw this is or whether the shader takes the buffer path for it. If the read is on a branch that this draw never takes, the hardware never interprets the T# as a V#. Without a capture I can't tell.
Some context on why this read throws, in case it saves a search: the exact word 3 here, 0x91b00f2e, is already in tests/execution/BufferDescriptorRuntime.cpp. aa2abf801 ("ignore unused descriptor fields for scalar buffer loads", merged 13 Oct) made scalar buffer loads ignore word 3, and that test checks a scalar read with 0x91b00f2e still returns the buffer's data. The same test requires a vector or mixed access with that word to keep throwing buffer descriptor uses an unsupported type, because what the hardware does when a vector buffer instruction gets a descriptor whose TYPE is not a buffer has not been measured (zeros, a read through the address and size fields anyway, or undefined).
So the read here takes the vector path (scalar=0 ... read through vector memory in your print), which is the case that test keeps refusing on purpose. Lifting it would need either that hardware behaviour, or evidence that this draw never reaches the read, as you suggest. The throw happens when the stage's resources are materialized, before anything executes, so an unreached read fails the same way as a reached one.
The buffer slot doesn't always hold the same descriptor. In some runs it's a 3D T#: 20da2600 c3800000 001dc13f a1b00fac, with word 3's nibble 0xA. It is always buffer 2 of 3 in a pixel shader, read by vector memory loads.
The next stop is the same pattern on an image slot. With a local, unsubmitted change that binds such a read-only buffer as empty, the next stop is:
image descriptor is incompatible with the static runtime image interface:
descriptor format 60 decodes to {class 2, dim 4, ...}; modes: {class 1, dim 3, ...} ...
T# 20dc6600 c3c00000 001fc00f a1b00fac 0000003f 00700000 006b0000 00208a6b
There, a 3D T# sits in a slot that the shader statically uses as a 2D image.
So Unreal leaves 3D-texture descriptors in buffer and 2D slots of this pixel shader. Either the shader's accesses to those slots aren't behind a branch the capture can evaluate, or the branch is taken. I couldn't find the shader address: the throw is reached through ResourceMaterializer::Materialize, the Release build has no unwind info past that frame, and it doesn't go through DrawCapture's Materialize call. The RDNA2 ISA only says TYPE must be 0 for a V# and doesn't define a buffer read through a T#, so I haven't proposed a fix.
I measured what the hardware does with a vector buffer access through a V# whose TYPE (word 3 bits 31:30) isn't 0. That's the case here: 0x91b00f2e and 0xa1b00fac both have TYPE 2. AMD RDNA 2 iGPU (gfx1036), Windows driver 32.0.21036.18, with the HIP runner from #1281 (probes below).
Loads (buffer_load_dword and buffer_load_dwordx2 with offen; buffer_load_format_xyzw and buffer_load_dword with idxen):
The control V# (0x31016fac, TYPE 0) reads the data.
Changing only TYPE to 1, 2 or 3 makes every load leave its destination VGPRs untouched: they keep a 0xdeadbeef preset. 96 of 96 offset/index rows, num_records 0x1000 and 0x100.
The load does not return zero, which is the difference from an out-of-range load.
With this issue's two word 3 values and TYPE cleared to 0, all four loads return zero (the FORMAT field there is 0). With their own TYPE 2, the VGPRs stay untouched.
Stores and atomics (buffer_store_dword, buffer_atomic_add ... glc), 64 lanes, read back through the same V# with TYPE 0:
TYPE 0: all stores and adds land.
TYPE 1, 2, 3: memory is unchanged on every lane, and the atomic's return VGPR keeps its previous value (1).
So on this GPU a vector buffer instruction through a V# with TYPE ≠ 0 does nothing: no memory access, no VGPR write. That matches the ISA's "TYPE must be 0" being enforced as a no-op rather than a fault. Scalar loads ignore word 3 already, as YossiYad noted. PS5's gfx1013 isn't measured.
Onimusha: Way of the Sword (PPSA27836, #5769) hits the same throw: Capcom's RE Engine, after the EULA screen.
probe_buffer_type.py
"""Buffer loads through a V# whose word 3 TYPE field (bits 31:30) is not 0, compared with TYPE 0.The V# points at a block of distinct words. Word 3 keeps the low 30 bits of a descriptor seen in atitle (DST_SEL, FORMAT, ...) and only TYPE changes. Each row is (byte offset, index, 0, 0)."""importstructimportsysfrompathlibimportPathsys.path.insert(0, str(Path(__file__).resolve().parent))
importhw_oracle_winasoracleROWS=128DATA_OFFSET=ROWS*16DATA_BYTES=8192STRIDE=16RECORDS=256OFFSETS= [0, 4, 8, 12, 16, 64, 1020, 4092, 4096, 8188, 8192, 12000]
INDICES= [0, 1, 2, 63, 255, 256, 300, 511]
WORD3_LOW= {
"control word 3 0x31016fac": 0x31016fac&0x3fffffff,
"PPSA02100 word 3 0x91b00f2e": 0x91b00f2e&0x3fffffff,
"PPSA02100 word 3 0xa1b00fac": 0xa1b00fac&0x3fffffff,
}
defbody(word3, num_records):
returnf""" s_add_u32 s16, s4, {DATA_OFFSET:#x} s_addc_u32 s17, s5, 0 s_and_b32 s17, s17, 0xffff s_or_b32 s17, s17, {STRIDE<<16:#x} s_mov_b32 s18, {num_records:#x} s_mov_b32 s19, {word3:#x} v_mov_b32 v10, 0xdeadbeef v_mov_b32 v11, 0xdeadbeef v_mov_b32 v12, 0xdeadbeef v_mov_b32 v13, 0xdeadbeef v_mov_b32 v14, 0xdeadbeef v_mov_b32 v15, 0xdeadbeef v_mov_b32 v16, 0xdeadbeef v_mov_b32 v17, 0xdeadbeef v_mov_b32 v18, 0xdeadbeef buffer_load_dword v10, v4, s[16:19], 0 offen buffer_load_dwordx2 v[11:12], v4, s[16:19], 0 offen buffer_load_format_xyzw v[14:17], v5, s[16:19], 0 idxen buffer_load_dword v18, v5, s[16:19], 0 idxen s_waitcnt vmcnt(0)"""defdata():
returnb"".join(struct.pack("<I", (i*0x9E3779B1+0x5A5A1234) &0xFFFFFFFF) foriinrange(DATA_BYTES//4))
defmain():
rows= [(o, i, 0, 0) foroinOFFSETSforiinINDICES]
count=len(rows)
rows=rows+ [(0, 0, 0, 0)] * (ROWS-count)
print(f"target {oracle.target()}; {count} rows; buffer loads through a V# with TYPE 0, 1, 2, 3")
forname, lowinWORD3_LOW.items():
fornum_recordsin (RECORDS*STRIDE, RECORDS):
results= {}
fortype_bitsinrange(4):
word3=low| (type_bits<<30)
results[type_bits] = [r[0:9] forrinoracle.run(body(word3, num_records), rows, data(), coarse=True)][:count]
base=results[0]
fortype_bitsin (1, 2, 3):
same=sum(1fora, binzip(base, results[type_bits]) ifa==b)
print(f"{name}, num_records {num_records:#x}: TYPE {type_bits} vs TYPE 0: {same}/{count} rows equal")
shown=0forrow, a, binzip(rows, base, results[type_bits]):
ifa!=bandshown<3:
shown+=1print(f" offset {row[0]} index {row[1]}: TYPE 0 {' '.join(f'{w:08x}'forwina)} | TYPE {type_bits}{' '.join(f'{w:08x}'forwinb)}")
sample=base[1]
print(f" TYPE 0 sample row offset {rows[1][0]} index {rows[1][1]}: {' '.join(f'{w:08x}'forwinsample)}")
if__name__=="__main__":
main()
probe_buffer_type_store.py
"""Buffer stores and atomics through a V# whose TYPE (word 3 bits 31:30) is not 0.Each lane stores, then adds, through the V# with the TYPE under test, then reads back the same dwordthrough the same V# with TYPE 0. Row: (byte offset, value, 0, 0); offsets are distinct per lane."""importstructimportsysfrompathlibimportPathsys.path.insert(0, str(Path(__file__).resolve().parent))
importhw_oracle_winasoracleROWS=64DATA_OFFSET=ROWS*16DATA_BYTES=4096WORD3=0x31016facdefbody(type_bits):
word3= (WORD3&0x3fffffff) | (type_bits<<30)
returnf""" s_add_u32 s16, s4, {DATA_OFFSET:#x} s_addc_u32 s17, s5, 0 s_and_b32 s17, s17, 0xffff s_mov_b32 s18, {DATA_BYTES:#x} s_mov_b32 s19, {word3:#x} s_mov_b32 s20, s16 s_mov_b32 s21, s17 s_mov_b32 s22, {DATA_BYTES:#x} s_mov_b32 s23, {WORD3:#x} buffer_store_dword v5, v4, s[16:19], 0 offen s_waitcnt vmcnt(0) buffer_load_dword v10, v4, s[20:23], 0 offen s_waitcnt vmcnt(0) v_mov_b32 v6, 1 buffer_atomic_add v6, v4, s[16:19], 0 offen offset:2048 glc s_waitcnt vmcnt(0) v_mov_b32 v11, v6 buffer_load_dword v12, v4, s[20:23], 0 offen offset:2048 s_waitcnt vmcnt(0)"""defmain():
rows= [(lane*4, 0x1000+lane, 0, 0) forlaneinrange(ROWS)]
data=b"".join(struct.pack("<I", 0xC0DE0000+i) foriinrange(DATA_BYTES//4))
print(f"target {oracle.target()}; {ROWS} lanes; store, then atomic add 1, through a V# with each TYPE")
fortype_bitsinrange(4):
results=oracle.run(body(type_bits), rows, data, coarse=True)
stored=sum(1forrow, rinzip(rows, results) ifr[0] ==row[1])
kept=sum(1forrow, rinzip(rows, results) ifr[0] ==0xC0DE0000+row[0] //4)
added=sum(1forrow, rinzip(rows, results) ifr[2] ==0xC0DE0000+ (2048+row[0]) //4+1)
untouched=sum(1forrow, rinzip(rows, results) ifr[2] ==0xC0DE0000+ (2048+row[0]) //4)
returned=sorted({r[1] forrinresults})[:3]
print(f"TYPE {type_bits}: store landed {stored}/{ROWS}, memory kept {kept}/{ROWS}; atomic added {added}/{ROWS}, memory kept {untouched}/{ROWS}; atomic returned {' '.join(f'{w:08x}'forwinreturned)}")
if__name__=="__main__":
main()
Output
target gfx1036; 96 rows; buffer loads through a V# with TYPE 0, 1, 2, 3
control word 3 0x31016fac, num_records 0x1000: TYPE 1 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 5a5a1234 5a5a1234 f8918be5 deadbeef 5a5a1234 5a5a1234 5a5a1234 5a5a1234 5a5a1234 | TYPE 1 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
control word 3 0x31016fac, num_records 0x1000: TYPE 2 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 5a5a1234 5a5a1234 f8918be5 deadbeef 5a5a1234 5a5a1234 5a5a1234 5a5a1234 5a5a1234 | TYPE 2 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
control word 3 0x31016fac, num_records 0x1000: TYPE 3 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 5a5a1234 5a5a1234 f8918be5 deadbeef 5a5a1234 5a5a1234 5a5a1234 5a5a1234 5a5a1234 | TYPE 3 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
control word 3 0x31016fac, num_records 0x100: TYPE 1 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 5a5a1234 5a5a1234 f8918be5 deadbeef 5a5a1234 5a5a1234 5a5a1234 5a5a1234 5a5a1234 | TYPE 1 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
control word 3 0x31016fac, num_records 0x100: TYPE 2 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 5a5a1234 5a5a1234 f8918be5 deadbeef 5a5a1234 5a5a1234 5a5a1234 5a5a1234 5a5a1234 | TYPE 2 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
control word 3 0x31016fac, num_records 0x100: TYPE 3 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 5a5a1234 5a5a1234 f8918be5 deadbeef 5a5a1234 5a5a1234 5a5a1234 5a5a1234 5a5a1234 | TYPE 3 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0x91b00f2e, num_records 0x1000: TYPE 1 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 1 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0x91b00f2e, num_records 0x1000: TYPE 2 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 2 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0x91b00f2e, num_records 0x1000: TYPE 3 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 3 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0x91b00f2e, num_records 0x100: TYPE 1 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 1 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0x91b00f2e, num_records 0x100: TYPE 2 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 2 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0x91b00f2e, num_records 0x100: TYPE 3 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 3 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0xa1b00fac, num_records 0x1000: TYPE 1 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 1 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0xa1b00fac, num_records 0x1000: TYPE 2 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 2 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0xa1b00fac, num_records 0x1000: TYPE 3 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 3 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0xa1b00fac, num_records 0x100: TYPE 1 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 1 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0xa1b00fac, num_records 0x100: TYPE 2 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 2 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
PPSA02100 word 3 0xa1b00fac, num_records 0x100: TYPE 3 vs TYPE 0: 0/96 rows equal
offset 0 index 0: TYPE 0 00000000 00000000 00000000 deadbeef 00000000 00000000 00000000 00000000 00000000 | TYPE 3 deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef deadbeef
target gfx1036; 64 lanes; store, then atomic add 1, through a V# with each TYPE
TYPE 0: store landed 64/64, memory kept 0/64; atomic added 64/64, memory kept 0/64; atomic returned c0de0200 c0de0201 c0de0202
TYPE 1: store landed 0/64, memory kept 64/64; atomic added 0/64, memory kept 64/64; atomic returned 00000001
TYPE 2: store landed 0/64, memory kept 64/64; atomic added 0/64, memory kept 64/64; atomic returned 00000001
TYPE 3: store landed 0/64, memory kept 64/64; atomic added 0/64, memory kept 64/64; atomic returned 00000001
@p3n1cillin on the shader address: the throw is reached from the resource capture, shaderMemory.Capture(invocation) in Driver::materializeDrawStage (the capture materializes the snapshot), which is why the Materialize call there never sees it. #5897 puts shader 0x<address> stage <n>: on failures of both calls, adds the buffer slot and V# words to this message, and with APS5_DUMP_SHADERS=1 dumps the stage's request. A run with it should name the shader and give a .req to replay.
@p3n1cillin, before anything is built on the TYPE ≠ 0 result @simonbronk measured above, it's worth checking whether this draw should run its pixel stage at all. Could you run current main with #5631 by @moong15 (head 75083acb3) applied? It skips the pixel stage for draws in the state described below. In its matched Stellar Blade runs, 2 of 6 runs on main stopped at presentation on buffer descriptor uses an unsupported type and none of the 6 with the PR did (runs in the PR). The PR notes that this link is inferred from the failure type, not traced to a draw.
In Stellar Blade, depth-only draws clear CB_TARGET_MASK, CB_SHADER_MASK and DB_SHADER_CONTROL but leave the previous pixel program bound and SPI_SHADER_Z_FORMAT non-zero.
That fits what you found: a buffer slot that holds a different T# from run to run, and then a 3D T# in a 2D slot of the same shader once you bound the buffer as empty.
Please also apply #5897 in the same build: as @YossiYad explains above, its message then names the shader and the V# if the stop remains. If you add a local print at that draw, 0x8e, 0x8f, 0x203 and 0x2dc (CB_TARGET_MASK, CB_SHADER_MASK, DB_SHADER_CONTROL, DB_ALPHA_TO_MASK) are the context registers #5631's check reads. Both PRs merge into maine34c47921 without conflicts, separately and together.
AI-assisted: written with Claude Code; I checked #5631's predicate change against PixelProgramSkipped on maine34c47921, where DecodeGraphicsPrograms substitutes the null pixel program, the Stellar Blade runs in #5631's body and gist, and that #5897 wraps the capture call. I ran git merge-tree for #5631 and #5897 against maine34c47921, each alone and both together. I did not run Stray or build these PRs.
@nikowolf123 thanks, that was it. With #5631 (head 75083acb3) and #5897 merged onto main4c1de0c0c, Stray gets past the loading screen into gameplay in 2 of 2 runs (250 s and 300 s). Neither the buffer-slot stop nor the image-slot stop comes up. Plain main stopped on buffer descriptor uses an unsupported type in all 6 of my runs. Details are in #5631. I'd leave this issue open until #5631 merges; @simonbronk's TYPE != 0 measurement then isn't needed for Stray.
Status update: #5631 has now merged (commit 75083ac). The reported Stray stop was cleared in the reporter's 2/2 runs with #5631 and #5897, so the condition in the last update (leave this issue open until #5631 merges) is now met.
Regs 8..11 differ on every draw and contain things that are not descriptors at all, for example c0017a00 20000243 00000400 c0033500 (PM4 headers), 00000001 00000008 00000000 bd000000, 0000008c 5d0c0000 00100020 00011800. Those are leftovers from earlier draws that used the same registers. The SET_SH_REG packets of these draws write only part of the bank. The indirect register lists were not the cause: I compared the list read at submit with a live read at execution and they were identical.
That fits the symptom in this issue and in #5769: the error changes from run to run (type 2, reserved bit, format 0, unaligned base, unreadable range 0x1d62a0000+0x200000, "exceeds descriptor range limit") because the stale words differ. The game works on the console, so the shader does not reach a vector access through that V#, or reaches it harmlessly.
Hardware behaviour is already measured above (loads leave the destination VGPRs untouched, stores and atomics do nothing, for TYPE 1..3). For a V# whose TYPE is 0 but whose other fields are junk (format 0, reserved bit 30 of word 1, a base that is not mapped), I have not measured anything.
Proposals
Mirror fix(agc): treat a TYPE 1 T# in a read-only static image slot as null #5828 for buffers: a V# with TYPE != 0 in a read-only slot binds as an empty buffer instead of failing the draw. The measured no-op is close to "reads return zero" and drops writes and atomics, so one rule covers loads, stores and atomics, and the existing test that requires the throw changes to require the empty binding.
The same for the other validations that read the same stale words: reserved bit 30 of word 1, FORMAT 0 or above the last known format on a typed access, a base that is not aligned to the element, and a range that is not readable. For the unreadable case addGuestBuffer already clamps to RegisteredReadableEnd when the size exceeds maxStorageBufferRange; doing it for any size, with 0 meaning the empty buffer, removes the guest memory is not readable throw.
If the maintainers prefer to keep failing on a descriptor the shader really reads: record, per user-data register, whether it was written by the packets of this draw or dispatch, and refuse only when the bad V# lives in registers written for it. Registers that carry data from an earlier draw would bind as empty. This keeps the strict behaviour for a game that really passes a broken descriptor, at the cost of tracking one bit per register.
I ran Kena with 1 and 2 applied locally (not a PR: the existing tests require the throw). Without them it stops after Continue; with them it stays up past Continue and reaches the loading screen (the in-game world itself I have not reached yet). It still ends later, in a run with frame dumping on, on AGC graphics: vertex fetch exceeds descriptor record count, which looks like the same family on a vertex buffer, and that one I have not looked into yet.
Menu and the loading screen after Continue with the local change (Kena 1.16, Windows, RTX 4070 Super):
If one of these is acceptable I can turn it into a PR with the test changes.
Thanks for the new Kena trace. It separates this from the original Stray stop, which was cleared by #5631: here the slot contains stale user-data words not written by the current draw, and eager materialization rejects it before execution. Your RDNA2 probe gives concrete evidence for the narrow TYPE != 0 case: vector loads leave destinations untouched, and stores/atomics do not modify memory. That supports testing a null/empty binding for non-buffer TYPEs, while keeping the broader TYPE=0 malformed-field relaxations out until they have separate evidence. Since you have the reproducer and hardware probe, a focused PR from your branch would preserve that provenance and let reviewers evaluate the behavior and test changes together.
One overlap note before implementation: #6412 is still open and defers TYPE rejection for guarded static buffer reads in eligible compute shaders. It does not cover this pixel-stage case or establish the no-op result, but it touches the same materialization/recompiler area, so please compare and coordinate with that author before choosing the implementation boundary.
I want to narrow my earlier suggestion about binding a null/empty buffer for a non-buffer TYPE. The RDNA2 probe shows that vector loads leave their destination VGPRs unchanged; a plain empty Vulkan buffer binding would return zeros, so it would not reproduce that measured behavior. The PS5 gfx1013 behavior is still unmeasured.
A conservative implementation direction is to defer rejection only for eligible, read-only, unformatted plain vector loads: bind a safe placeholder and record an invalid-descriptor fault if an active fragment actually executes the load, then report it at draw completion. If control flow skips the load, no fault is raised. This avoids emulating the RDNA2 no-op on PS5. It would extend the planning/materialization/fault machinery in #6412 from compute to pixel shaders, so I would coordinate the boundary with that PR's author before opening a stacked follow-up. I have not implemented or validated this change.
Following up on the Kena fragment-stage case: are you planning to submit the guarded-read change from your reproducer branch, or would you prefer that I prepare a separate follow-up after #6412 is coordinated and rebased? I will hold off on overlapping implementation until the ownership boundary and #6412 sequencing are clear. The conservative test scope remains an inactive guarded read that completes without a fault and an active guarded read that reports an InvalidDescriptor fault at its load PC.
Thanks. Please go ahead with #7689; I'm not going to submit a competing PR. My local change just binds empty buffers for those slots, which doesn't match the hardware no-op, so your deferred-fault approach is the better boundary. I have a Windows machine with an RTX 4070 Super and the Kena dump, so once #6412 and #7689 are stacked I can run Kena on them and report whether it gets past Continue. Separately, a later run still stopped on AGC graphics: vertex fetch exceeds descriptor record count. That looks like the same stale-register family on a vertex buffer, and I'll file it separately rather than expand this one.
Current maind6eafa54d, Stray (PPSA02100) 01.002.000, Windows 11 Pro, RTX 5080 (driver 32.0.16.1742): buffer descriptor uses an unsupported type did not occur in 18 runs on main (2 with a cold shader cache, 60-900 s, 5 of them through the first level load into gameplay). The Stray case of this issue looks fixed by #5631, as reported above; what's left here is the Kena (PPSA01802) stale user-data case that #7689 / #6412 address.
Preserved mesh-gameplay observation, 2026-10-23 20:44 UTC:fork a9ba30003d/main61ad again reaches daytime gameplay, movement, ring collection, raised platforms, pots and32rings. At the cliff with a spring pillar ahead it exits naturally after2318.152s, before its2700s cap:
shader 0x20bd27400 stage 7: buffer descriptor uses an unsupported type (buffer 5)
Stage7 is mesh. The shader address differs from the earlier capture below; matching stage/buffer/refusal text is not proof of identical descriptor provenance or root cause, including Stray/Kena. Cross/Square/forward inputs preceded the stop; no causal input claim. The36,888-byte request/raw words remain local. No guard was relaxed; the source-expression diagnostic only affects failed image descriptions and did not fire here. Earlier eligibility/conditional-path analysis below has not been replayed against this new request and is not silently transferred to it.
Full current device, validation, artifact identity and limits: RTX5080/610.62, Win11Enterprise26200, loaded Khronos1.4.363;368libraries/app verified before/after;0 validation errors/VUIDs,20documented warnings. Four submission-watchdog messages across two submission IDs, two fence failures/two not-ready queries; rendering recovers from the first episode, second precedes abort. No host-device-loss or unchanged-main gameplay claim. Log SHA256:5ae7bfa8736003eb555a8fa56793a19916f4f878777507565330eec2e591bc00. Bring-up discussion. AI-assisted:yes(Codex).
Prepared follow-up diagnostic:fork184b0b4684 incorporates main11e8ad293422aad5e30e6c8f5c85048d4dc56649 including #8713, and adds failure-only buffer source/first-use/TYPE/access flags plus the bounded word3 expression description already used for image failures. It preserves the existing rejection and performs no extra guest read/evaluation. 40positive checks on literal main/composition, both GPUs;3intended missing-metadata controls;full logs. Original main fixture restored/rebuilt and passes separately. Advisory analysis found3Cppcheck/3clang findings at unchanged main fixture contexts; no new defect established. Subsequent11e8/e7f/a23/11d6 runs and the latest af426/maindb6 diagnostic replay stop earlier at #3036. The latest ends naturally after412.217s during level loading, without gameplay; the buffer diagnostic was not exercised. This does not fix or independently retest the preserved mesh refusal. Current component qualification remains separate from title evidence.
Earlier 60c0/main0db4 observation and its separately scoped analysis
Additional title/stage observation: Sonic Unleashed Recompiled (PPSA99902), mesh stage. I am recording it here because the refusal text matches; I have not established the same root cause as Stray/Kena.
The composed fork 60c0cc1845, incorporating main 0db47b263d1019043f7d28fad4e7ee0a1ea5ec6a, reaches daytime gameplay beyond the pots, 32 rings, and the Homing Attack instruction. After dismissing the instruction and queuing Cross (300 ms), then Square (1000 ms starting at +600 ms), it exits naturally after 1391.511 seconds, before the 1800-second cap:
shader 0x20bea8f00 stage 7: buffer descriptor uses an unsupported type
Stage 7 is ShaderStage::Mesh. The full local diagnostic identifies buffer 5; request contents remain local. The abort is recorded as std::runtime_error followed by SIGABRT, exit 0xc0000409. This is one composed-runtime run, not an unchanged-main title run or a claim the button sequence caused the failure. Retained application/guest-libc conversion and all 367 newly deployed native libraries were hash-verified.
Offline checks of this captured request reproduce the same refusal category on literal main 0db47b263d and the fork. Adding #8518 at 1c9a522bef (composition 81e3e646d8), then #8632 at f5d3f5a89d (composition cb60785b83), leaves that result unchanged. The captured-memory walk reports no missing reads. agc_shader_memory passes 3/3 on each PR composition; those CPU checks are not title validation of the PRs. #6412 and #7689 describe compute and fragment eligibility respectively; neither is demonstrated to cover this mesh case.
RTX 5080, NVIDIA 610.62 / 32.0.16.1062, Windows 11 Enterprise 26200, WinLibs GCC 15.2 r7/UCRT. Khronos 1.4.363 loaded: zero validation errors/VUIDs and the same 14 previously recorded warnings. Two guest-wait episodes recover before the fatal stop. This diagnostic run is not a benchmark.
Next investigation is whether the mesh draw actually consumes this descriptor and why this value reaches its buffer slot. The strict TYPE guard remains intact. Sonic bring-up context. AI-assisted: yes (Codex); author responsible.
Additional local classification: a counts-only query at fork f1b06d57ab377ef68f75841f20b53b41d10efdbf (the AGC/shader trees are unchanged from the run) identifies buffer 5 as one typed/formatted read, not a scalar load, write or atomic. The translated mesh program contains 33 shared-memory instructions and four barriers. CPU capture considers its source active; this does not prove a GPU lane executes the load. These properties put the case outside the documented plain-load/no-LDS/no-barrier eligibility of #6412, in addition to the stage difference from #6412/#7689. A mesh-stage toggle alone is therefore not an established solution. Descriptor provenance and the executing path still need investigation.
Conditional-path check: a further local counts-only comparison produces identical metadata on literal main 0a0ed7216784cc5cdbadc450985d179d50ef2d03 and fork f1b06d57ab377ef68f75841f20b53b41d10efdbf. In the translated resource CFG, the branch separating the buffer-5 source from the other path is not CPU-evaluable. Its expression includes lane/wave, mesh and shared-memory dependencies. This establishes why the current planner conservatively includes the source; it does not establish that a GPU lane takes the path, that every dependency is irreducible, or that the descriptor is valid at execution. No safe CPU-only pruning rule or mesh-stage deferred-fault fix has been established. The same TYPE refusal remains, with no missing reads in this local capture check. Shader/request contents and assets remain private. This is local preparation evidence, not a fresh title run or a new root-cause claim.
Activity
YossiYad commentedon Oct 14, 2026
Some context on why this read throws, in case it saves a search: the exact word 3 here,
0x91b00f2e, is already intests/execution/BufferDescriptorRuntime.cpp.aa2abf801("ignore unused descriptor fields for scalar buffer loads", merged 13 Oct) made scalar buffer loads ignore word 3, and that test checks a scalar read with0x91b00f2estill returns the buffer's data. The same test requires a vector or mixed access with that word to keep throwingbuffer descriptor uses an unsupported type, because what the hardware does when a vector buffer instruction gets a descriptor whose TYPE is not a buffer has not been measured (zeros, a read through the address and size fields anyway, or undefined).So the read here takes the vector path (
scalar=0 ... read through vector memoryin your print), which is the case that test keeps refusing on purpose. Lifting it would need either that hardware behaviour, or evidence that this draw never reaches the read, as you suggest. The throw happens when the stage's resources are materialized, before anything executes, so an unreached read fails the same way as a reached one.p3n1cillin commentedon Oct 14, 2026
More detail from later runs, same setup:
20da2600 c3800000 001dc13f a1b00fac, with word 3's nibble0xA. It is always buffer 2 of 3 in a pixel shader, read by vector memory loads.So Unreal leaves 3D-texture descriptors in buffer and 2D slots of this pixel shader. Either the shader's accesses to those slots aren't behind a branch the capture can evaluate, or the branch is taken. I couldn't find the shader address: the throw is reached through
ResourceMaterializer::Materialize, the Release build has no unwind info past that frame, and it doesn't go throughDrawCapture'sMaterializecall. The RDNA2 ISA only saysTYPEmust be 0 for a V# and doesn't define a buffer read through a T#, so I haven't proposed a fix.simonbronk commentedon Oct 14, 2026
I measured what the hardware does with a vector buffer access through a V# whose
TYPE(word 3 bits 31:30) isn't 0. That's the case here:0x91b00f2eand0xa1b00facboth have TYPE 2. AMD RDNA 2 iGPU (gfx1036), Windows driver 32.0.21036.18, with the HIP runner from #1281 (probes below).Loads (
buffer_load_dwordandbuffer_load_dwordx2withoffen;buffer_load_format_xyzwandbuffer_load_dwordwithidxen):0x31016fac, TYPE 0) reads the data.0xdeadbeefpreset. 96 of 96 offset/index rows, num_records0x1000and0x100.Stores and atomics (
buffer_store_dword,buffer_atomic_add ... glc), 64 lanes, read back through the same V# with TYPE 0:So on this GPU a vector buffer instruction through a V# with TYPE ≠ 0 does nothing: no memory access, no VGPR write. That matches the ISA's "TYPE must be 0" being enforced as a no-op rather than a fault. Scalar loads ignore word 3 already, as YossiYad noted. PS5's gfx1013 isn't measured.
Onimusha: Way of the Sword (PPSA27836, #5769) hits the same throw: Capcom's RE Engine, after the EULA screen.
probe_buffer_type.py
probe_buffer_type_store.py
Output
AI-assisted (Claude Code).
YossiYad commentedon Oct 14, 2026
@p3n1cillin on the shader address: the throw is reached from the resource capture,
shaderMemory.Capture(invocation)inDriver::materializeDrawStage(the capture materializes the snapshot), which is why theMaterializecall there never sees it. #5897 putsshader 0x<address> stage <n>:on failures of both calls, adds the buffer slot and V# words to this message, and withAPS5_DUMP_SHADERS=1dumps the stage's request. A run with it should name the shader and give a.reqto replay.nikowolf123 commentedon Oct 14, 2026
@p3n1cillin, before anything is built on the TYPE ≠ 0 result @simonbronk measured above, it's worth checking whether this draw should run its pixel stage at all. Could you run current
mainwith #5631 by @moong15 (head75083acb3) applied? It skips the pixel stage for draws in the state described below. In its matched Stellar Blade runs, 2 of 6 runs onmainstopped at presentation onbuffer descriptor uses an unsupported typeand none of the 6 with the PR did (runs in the PR). The PR notes that this link is inferred from the failure type, not traced to a draw.What #5631 changes, and why it could fit here:
CB_TARGET_MASK,CB_SHADER_MASKandDB_SHADER_CONTROLbut leave the previous pixel program bound andSPI_SHADER_Z_FORMATnon-zero.mainskips the pixel stage only when Z_FORMAT is also 0 (State.cpp#L1404-L1415). Otherwise it materializes that program with the current user data, which fix(agc): skip unused pixel stages with retained export formats #5631 found was written for a different program.75083acb3), and such a draw then gets the null pixel program before any of its resources are read (DrawDecode.cpp#L27-L30). If Stray's draw is in that state, the image-slot stop you found next should go away too, which a TYPE ≠ 0 rule for buffers would not cover. Whether it is in that state is not known; the run would show it.Please also apply #5897 in the same build: as @YossiYad explains above, its message then names the shader and the V# if the stop remains. If you add a local print at that draw,
0x8e,0x8f,0x203and0x2dc(CB_TARGET_MASK,CB_SHADER_MASK,DB_SHADER_CONTROL,DB_ALPHA_TO_MASK) are the context registers #5631's check reads. Both PRs merge intomaine34c47921without conflicts, separately and together.AI-assisted: written with Claude Code; I checked #5631's predicate change against
PixelProgramSkippedonmaine34c47921, whereDecodeGraphicsProgramssubstitutes the null pixel program, the Stellar Blade runs in #5631's body and gist, and that #5897 wraps the capture call. I rangit merge-treefor #5631 and #5897 againstmaine34c47921, each alone and both together. I did not run Stray or build these PRs.p3n1cillin commentedon Oct 14, 2026
@nikowolf123 thanks, that was it. With #5631 (head
75083acb3) and #5897 merged ontomain4c1de0c0c, Stray gets past the loading screen into gameplay in 2 of 2 runs (250 s and 300 s). Neither the buffer-slot stop nor the image-slot stop comes up. Plainmainstopped onbuffer descriptor uses an unsupported typein all 6 of my runs. Details are in #5631. I'd leave this issue open until #5631 merges; @simonbronk'sTYPE != 0measurement then isn't needed for Stray.MarcZX10 commentedon Oct 15, 2026
Status update: #5631 has now merged (commit 75083ac). The reported Stray stop was cleared in the reporter's 2/2 runs with #5631 and #5897, so the condition in the last update (leave this issue open until #5631 merges) is now met.
ymatrix1 commentedon Oct 16, 2026
Same throw on Kena: Bridge of Spirits (PPSA01802), after Continue, and I think I can say where the descriptor comes from.
The V# is a user-data register the game never set for that draw
The failing shader declares four buffers. Dumping the user data of every failing materialization showed:
Regs 8..11 differ on every draw and contain things that are not descriptors at all, for example
c0017a00 20000243 00000400 c0033500(PM4 headers),00000001 00000008 00000000 bd000000,0000008c 5d0c0000 00100020 00011800. Those are leftovers from earlier draws that used the same registers. The SET_SH_REG packets of these draws write only part of the bank. The indirect register lists were not the cause: I compared the list read at submit with a live read at execution and they were identical.That fits the symptom in this issue and in #5769: the error changes from run to run (type 2, reserved bit, format 0, unaligned base, unreadable range
0x1d62a0000+0x200000, "exceeds descriptor range limit") because the stale words differ. The game works on the console, so the shader does not reach a vector access through that V#, or reaches it harmlessly.Hardware behaviour is already measured above (loads leave the destination VGPRs untouched, stores and atomics do nothing, for TYPE 1..3). For a V# whose TYPE is 0 but whose other fields are junk (format 0, reserved bit 30 of word 1, a base that is not mapped), I have not measured anything.
Proposals
addGuestBufferalready clamps toRegisteredReadableEndwhen the size exceedsmaxStorageBufferRange; doing it for any size, with 0 meaning the empty buffer, removes theguest memory is not readablethrow.I ran Kena with 1 and 2 applied locally (not a PR: the existing tests require the throw). Without them it stops after Continue; with them it stays up past Continue and reaches the loading screen (the in-game world itself I have not reached yet). It still ends later, in a run with frame dumping on, on
AGC graphics: vertex fetch exceeds descriptor record count, which looks like the same family on a vertex buffer, and that one I have not looked into yet.Menu and the loading screen after Continue with the local change (Kena 1.16, Windows, RTX 4070 Super):
If one of these is acceptable I can turn it into a PR with the test changes.
MarcZX10 commentedon Oct 16, 2026
Thanks for the new Kena trace. It separates this from the original Stray stop, which was cleared by #5631: here the slot contains stale user-data words not written by the current draw, and eager materialization rejects it before execution. Your RDNA2 probe gives concrete evidence for the narrow TYPE != 0 case: vector loads leave destinations untouched, and stores/atomics do not modify memory. That supports testing a null/empty binding for non-buffer TYPEs, while keeping the broader TYPE=0 malformed-field relaxations out until they have separate evidence. Since you have the reproducer and hardware probe, a focused PR from your branch would preserve that provenance and let reviewers evaluate the behavior and test changes together.
MarcZX10 commentedon Oct 16, 2026
One overlap note before implementation: #6412 is still open and defers TYPE rejection for guarded static buffer reads in eligible compute shaders. It does not cover this pixel-stage case or establish the no-op result, but it touches the same materialization/recompiler area, so please compare and coordinate with that author before choosing the implementation boundary.
MarcZX10 commentedon Oct 16, 2026
I want to narrow my earlier suggestion about binding a null/empty buffer for a non-buffer TYPE. The RDNA2 probe shows that vector loads leave their destination VGPRs unchanged; a plain empty Vulkan buffer binding would return zeros, so it would not reproduce that measured behavior. The PS5 gfx1013 behavior is still unmeasured.
A conservative implementation direction is to defer rejection only for eligible, read-only, unformatted plain vector loads: bind a safe placeholder and record an invalid-descriptor fault if an active fragment actually executes the load, then report it at draw completion. If control flow skips the load, no fault is raised. This avoids emulating the RDNA2 no-op on PS5. It would extend the planning/materialization/fault machinery in #6412 from compute to pixel shaders, so I would coordinate the boundary with that PR's author before opening a stacked follow-up. I have not implemented or validated this change.
MarcZX10 commentedon Oct 17, 2026
Following up on the Kena fragment-stage case: are you planning to submit the guarded-read change from your reproducer branch, or would you prefer that I prepare a separate follow-up after #6412 is coordinated and rebased? I will hold off on overlapping implementation until the ownership boundary and #6412 sequencing are clear. The conservative test scope remains an inactive guarded read that completes without a fault and an active guarded read that reports an InvalidDescriptor fault at its load PC.
ymatrix1 commentedon Oct 19, 2026
Thanks. Please go ahead with #7689; I'm not going to submit a competing PR. My local change just binds empty buffers for those slots, which doesn't match the hardware no-op, so your deferred-fault approach is the better boundary. I have a Windows machine with an RTX 4070 Super and the Kena dump, so once #6412 and #7689 are stacked I can run Kena on them and report whether it gets past Continue. Separately, a later run still stopped on
AGC graphics: vertex fetch exceeds descriptor record count. That looks like the same stale-register family on a vertex buffer, and I'll file it separately rather than expand this one.dantdmdarealguy commentedon Oct 22, 2026
Current
maind6eafa54d, Stray (PPSA02100) 01.002.000, Windows 11 Pro, RTX 5080 (driver 32.0.16.1742):buffer descriptor uses an unsupported typedid not occur in 18 runs onmain(2 with a cold shader cache, 60-900 s, 5 of them through the first level load into gameplay). The Stray case of this issue looks fixed by #5631, as reported above; what's left here is the Kena (PPSA01802) stale user-data case that #7689 / #6412 address.Amoenus commentedon Oct 23, 2026
Preserved mesh-gameplay observation, 2026-10-23 20:44 UTC: fork a9ba30003d/main61ad again reaches daytime gameplay, movement, ring collection, raised platforms, pots and32rings. At the cliff with a spring pillar ahead it exits naturally after2318.152s, before its2700s cap:
Stage7 is mesh. The shader address differs from the earlier capture below; matching stage/buffer/refusal text is not proof of identical descriptor provenance or root cause, including Stray/Kena. Cross/Square/forward inputs preceded the stop; no causal input claim. The36,888-byte request/raw words remain local. No guard was relaxed; the source-expression diagnostic only affects failed image descriptions and did not fire here. Earlier eligibility/conditional-path analysis below has not been replayed against this new request and is not silently transferred to it.
Full current device, validation, artifact identity and limits: RTX5080/610.62, Win11Enterprise26200, loaded Khronos1.4.363;368libraries/app verified before/after;0 validation errors/VUIDs,20documented warnings. Four submission-watchdog messages across two submission IDs, two fence failures/two not-ready queries; rendering recovers from the first episode, second precedes abort. No host-device-loss or unchanged-main gameplay claim. Log SHA256:
5ae7bfa8736003eb555a8fa56793a19916f4f878777507565330eec2e591bc00. Bring-up discussion. AI-assisted:yes(Codex).Prepared follow-up diagnostic: fork184b0b4684 incorporates main
11e8ad293422aad5e30e6c8f5c85048d4dc56649including #8713, and adds failure-only buffer source/first-use/TYPE/access flags plus the bounded word3 expression description already used for image failures. It preserves the existing rejection and performs no extra guest read/evaluation. 40positive checks on literal main/composition, both GPUs;3intended missing-metadata controls;full logs. Original main fixture restored/rebuilt and passes separately. Advisory analysis found3Cppcheck/3clang findings at unchanged main fixture contexts; no new defect established. Subsequent11e8/e7f/a23/11d6 runs and the latest af426/maindb6 diagnostic replay stop earlier at #3036. The latest ends naturally after412.217s during level loading, without gameplay; the buffer diagnostic was not exercised. This does not fix or independently retest the preserved mesh refusal. Current component qualification remains separate from title evidence.Earlier 60c0/main0db4 observation and its separately scoped analysis
Additional title/stage observation: Sonic Unleashed Recompiled (PPSA99902), mesh stage. I am recording it here because the refusal text matches; I have not established the same root cause as Stray/Kena.
The composed fork
60c0cc1845, incorporating main0db47b263d1019043f7d28fad4e7ee0a1ea5ec6a, reaches daytime gameplay beyond the pots, 32 rings, and the Homing Attack instruction. After dismissing the instruction and queuing Cross (300 ms), then Square (1000 ms starting at +600 ms), it exits naturally after 1391.511 seconds, before the 1800-second cap:Stage 7 is
ShaderStage::Mesh. The full local diagnostic identifies buffer 5; request contents remain local. The abort is recorded asstd::runtime_errorfollowed by SIGABRT, exit0xc0000409. This is one composed-runtime run, not an unchanged-main title run or a claim the button sequence caused the failure. Retained application/guest-libc conversion and all 367 newly deployed native libraries were hash-verified.Offline checks of this captured request reproduce the same refusal category on literal main
0db47b263dand the fork. Adding #8518 at1c9a522bef(composition81e3e646d8), then #8632 atf5d3f5a89d(compositioncb60785b83), leaves that result unchanged. The captured-memory walk reports no missing reads.agc_shader_memorypasses 3/3 on each PR composition; those CPU checks are not title validation of the PRs. #6412 and #7689 describe compute and fragment eligibility respectively; neither is demonstrated to cover this mesh case.RTX 5080, NVIDIA 610.62 / 32.0.16.1062, Windows 11 Enterprise 26200, WinLibs GCC 15.2 r7/UCRT. Khronos 1.4.363 loaded: zero validation errors/VUIDs and the same 14 previously recorded warnings. Two guest-wait episodes recover before the fatal stop. This diagnostic run is not a benchmark.
Next investigation is whether the mesh draw actually consumes this descriptor and why this value reaches its buffer slot. The strict TYPE guard remains intact. Sonic bring-up context. AI-assisted: yes (Codex); author responsible.
Additional local classification: a counts-only query at fork
f1b06d57ab377ef68f75841f20b53b41d10efdbf(the AGC/shader trees are unchanged from the run) identifies buffer 5 as one typed/formatted read, not a scalar load, write or atomic. The translated mesh program contains 33 shared-memory instructions and four barriers. CPU capture considers its source active; this does not prove a GPU lane executes the load. These properties put the case outside the documented plain-load/no-LDS/no-barrier eligibility of #6412, in addition to the stage difference from #6412/#7689. A mesh-stage toggle alone is therefore not an established solution. Descriptor provenance and the executing path still need investigation.Conditional-path check: a further local counts-only comparison produces identical metadata on literal main
0a0ed7216784cc5cdbadc450985d179d50ef2d03and forkf1b06d57ab377ef68f75841f20b53b41d10efdbf. In the translated resource CFG, the branch separating the buffer-5 source from the other path is not CPU-evaluable. Its expression includes lane/wave, mesh and shared-memory dependencies. This establishes why the current planner conservatively includes the source; it does not establish that a GPU lane takes the path, that every dependency is irreducible, or that the descriptor is valid at execution. No safe CPU-only pruning rule or mesh-stage deferred-fault fix has been established. The same TYPE refusal remains, with no missing reads in this local capture check. Shader/request contents and assets remain private. This is local preparation evidence, not a fresh title run or a new root-cause claim.