Skip to content

Stray (PPSA02100): a pixel shader reads a 2D image descriptor as a buffer ("buffer descriptor uses an unsupported type") #5879

Open

Description

@p3n1cillin

Stray (PPSA02100, Unreal Engine 4) on Windows 11, RTX 5070, MinGW GCC 15, Release, main 066b58fa1, 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:

stage=2 (pixel) buffer 2 of 3
dwords = 20fd8300 c3800000 021bc3bf 91b00f2e
scalar=0 written=0 atomic=0 formatted=0 imageAlias=none, read through vector memory

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.

AI-assisted: yes (Claude Code)

Activity

  1. YossiYad commented on Oct 14, 2026

    @YossiYad
    Contributor

    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.

  2. p3n1cillin commented on Oct 14, 2026

    @p3n1cillin
    Author

    More detail from later runs, same setup:

    • 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.

  3. simonbronk commented on Oct 14, 2026

    @simonbronk
    Contributor

    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 a
    title (DST_SEL, FORMAT, ...) and only TYPE changes. Each row is (byte offset, index, 0, 0).
    """
    import struct
    import sys
    from pathlib import Path
    
    sys.path.insert(0, str(Path(__file__).resolve().parent))
    import hw_oracle_win as oracle
    
    ROWS = 128
    DATA_OFFSET = ROWS * 16
    DATA_BYTES = 8192
    STRIDE = 16
    RECORDS = 256
    
    OFFSETS = [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,
    }
    
    
    def body(word3, num_records):
        return f"""
      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)
    """
    
    
    def data():
        return b"".join(struct.pack("<I", (i * 0x9E3779B1 + 0x5A5A1234) & 0xFFFFFFFF) for i in range(DATA_BYTES // 4))
    
    
    def main():
        rows = [(o, i, 0, 0) for o in OFFSETS for i in INDICES]
        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")
        for name, low in WORD3_LOW.items():
            for num_records in (RECORDS * STRIDE, RECORDS):
                results = {}
                for type_bits in range(4):
                    word3 = low | (type_bits << 30)
                    results[type_bits] = [r[0:9] for r in oracle.run(body(word3, num_records), rows, data(), coarse=True)][:count]
                base = results[0]
                for type_bits in (1, 2, 3):
                    same = sum(1 for a, b in zip(base, results[type_bits]) if a == b)
                    print(f"{name}, num_records {num_records:#x}: TYPE {type_bits} vs TYPE 0: {same}/{count} rows equal")
                    shown = 0
                    for row, a, b in zip(rows, base, results[type_bits]):
                        if a != b and shown < 3:
                            shown += 1
                            print(f"  offset {row[0]} index {row[1]}: TYPE 0 {' '.join(f'{w:08x}' for w in a)} | TYPE {type_bits} {' '.join(f'{w:08x}' for w in b)}")
                sample = base[1]
                print(f"  TYPE 0 sample row offset {rows[1][0]} index {rows[1][1]}: {' '.join(f'{w:08x}' for w in sample)}")
    
    
    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 dword
    through the same V# with TYPE 0. Row: (byte offset, value, 0, 0); offsets are distinct per lane.
    """
    import struct
    import sys
    from pathlib import Path
    
    sys.path.insert(0, str(Path(__file__).resolve().parent))
    import hw_oracle_win as oracle
    
    ROWS = 64
    DATA_OFFSET = ROWS * 16
    DATA_BYTES = 4096
    WORD3 = 0x31016fac
    
    
    def body(type_bits):
        word3 = (WORD3 & 0x3fffffff) | (type_bits << 30)
        return f"""
      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)
    """
    
    
    def main():
        rows = [(lane * 4, 0x1000 + lane, 0, 0) for lane in range(ROWS)]
        data = b"".join(struct.pack("<I", 0xC0DE0000 + i) for i in range(DATA_BYTES // 4))
        print(f"target {oracle.target()}; {ROWS} lanes; store, then atomic add 1, through a V# with each TYPE")
        for type_bits in range(4):
            results = oracle.run(body(type_bits), rows, data, coarse=True)
            stored = sum(1 for row, r in zip(rows, results) if r[0] == row[1])
            kept = sum(1 for row, r in zip(rows, results) if r[0] == 0xC0DE0000 + row[0] // 4)
            added = sum(1 for row, r in zip(rows, results) if r[2] == 0xC0DE0000 + (2048 + row[0]) // 4 + 1)
            untouched = sum(1 for row, r in zip(rows, results) if r[2] == 0xC0DE0000 + (2048 + row[0]) // 4)
            returned = sorted({r[1] for r in results})[: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}' for w in returned)}")
    
    
    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
    

    AI-assisted (Claude Code).

  4. YossiYad commented on Oct 14, 2026

    @YossiYad
    Contributor

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

  5. nikowolf123 commented on Oct 14, 2026

    @nikowolf123
    Contributor

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

    What #5631 changes, and why it could fit here:

    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 main e34c47921 without conflicts, separately and together.

    AI-assisted: written with Claude Code; I checked #5631's predicate change against PixelProgramSkipped on main e34c47921, 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 main e34c47921, each alone and both together. I did not run Stray or build these PRs.

  6. p3n1cillin commented on Oct 14, 2026

    @p3n1cillin
    Author

    @nikowolf123 thanks, that was it. With #5631 (head 75083acb3) and #5897 merged onto main 4c1de0c0c, 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.

  7. MarcZX10 commented on Oct 15, 2026

    @MarcZX10
    Contributor

    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.

  8. ymatrix1 commented on Oct 16, 2026

    @ymatrix1
    Contributor

    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:

    userData: 01151b40 00040010 00000002 00014204 | 00e6e000 00040015 00000800 00014204 | 84254063 80262e5f 8a23b36c 88245269 | 4c21379c 00100020 00000001 0004dfac
    buf1 = regs 0..3   valid V#
    buf2 = regs 4..7   valid V#
    buf3 = regs 8..11  84254063 80262e5f 8a23b36c 88245269   <- the one that throws
    buf0 = regs 12..15 valid V# (a 16-byte per-draw constant buffer, stride 16, 1 record)
    

    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

    1. 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.
    2. 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.
    3. 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.
    4. Independently of 1..3, the refusal should name the user-data registers the V# came from, so the next report shows right away whether the slot was ever set. fix(agc): name the shader and V# when a draw is refused for a buffer descriptor type #5897 already names the shader and V#.

    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):

    Kena main menu

    Kena loading screen after Continue

    If one of these is acceptable I can turn it into a PR with the test changes.

  9. MarcZX10 commented on Oct 16, 2026

    @MarcZX10
    Contributor

    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.

  10. MarcZX10 commented on Oct 16, 2026

    @MarcZX10
    Contributor

    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.

  11. MarcZX10 commented on Oct 16, 2026

    @MarcZX10
    Contributor

    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.

  12. MarcZX10 commented on Oct 17, 2026

    @MarcZX10
    Contributor

    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.

  13. ymatrix1 commented on Oct 19, 2026

    @ymatrix1
    Contributor

    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.

  14. dantdmdarealguy commented on Oct 22, 2026

    @dantdmdarealguy
    Contributor

    Current main d6eafa54d, 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.

  15. Amoenus commented on Oct 23, 2026

    @Amoenus
    Contributor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions