From 986721f5bca49e20d0e3312782882429dd5cfcb6 Mon Sep 17 00:00:00 2001 From: MrFr3di Date: Tue, 27 Oct 2026 02:34:54 +0500 Subject: [PATCH] shader: decode FLAT, GLOBAL and SCRATCH D16 high-half loads (#1924) FLAT-family opcode 0x25 (FLAT/GLOBAL/SCRATCH_LOAD_SHORT_D16_HI) loads an unsigned halfword into bits [31:16] of the destination and keeps bits [15:0]. Decode it beside the low-half variant and let it select the high half of the existing partial destination write, so FLAT_LOAD needs no new path. Ghost of Yotei CS 0x79b0066f0cd1f3cd stopped at global_load_short_d16_hi v16, v18, s[0:1] offset:196 (0xdc9480c4 0x10000012). --- .../recompiler/frontend/decode/MemoryOps.cpp | 23 +- .../frontend/decode/ShaderDecoder.cpp | 1 + .../frontend/decode/ShaderDecoder.h | 1 + .../recompiler/frontend/translate/Memory.cpp | 1 + tests/ShaderRecompilerComputeTests.cpp | 218 +++++++++++++++--- tests/shaderCfgTests.cpp | 151 ++++++++++++ 6 files changed, 358 insertions(+), 37 deletions(-) diff --git a/src/graphics/shader/recompiler/frontend/decode/MemoryOps.cpp b/src/graphics/shader/recompiler/frontend/decode/MemoryOps.cpp index cca92e1d1..8ab805419 100644 --- a/src/graphics/shader/recompiler/frontend/decode/MemoryOps.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/MemoryOps.cpp @@ -95,14 +95,14 @@ constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = { }; constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = { - {0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true}, - {0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true}, - {0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32}, - {0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32}, - {0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16}, - {0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32}, - {0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32}, - {0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16}, + {0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true}, + {0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true}, + {0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32}, + {0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32}, + {0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16}, + {0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32}, + {0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32}, + {0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16}, {0x25u, Opcode::FLAT_LOAD_SHORT_D16_HI, 1, 16}, }; constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = { @@ -419,9 +419,10 @@ void DecodeFlat(uint32_t pc, std::span code, uint32_t word_index } DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst); - if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) { - // D16 loads reuse partial destinations to preserve the untouched high half. - inst.dst.sdwa_sel = 4u; + if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16 || + inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI) { + // D16 loads reuse partial destinations to preserve the untouched half. + inst.dst.sdwa_sel = inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI ? 5u : 4u; } DecodeVectorGpr(addr, inst.src0); inst.src_count = 1; diff --git a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp index 2b01fbb60..2a9611412 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp @@ -701,6 +701,7 @@ std::string InstructionToString(const Instruction& inst) { case Opcode::FLAT_LOAD_USHORT: case Opcode::FLAT_LOAD_SSHORT: case Opcode::FLAT_LOAD_SHORT_D16: + case Opcode::FLAT_LOAD_SHORT_D16_HI: case Opcode::FLAT_LOAD_DWORD: case Opcode::FLAT_LOAD_DWORDX2: case Opcode::FLAT_LOAD_DWORDX3: diff --git a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h index bb6083be0..d207026fa 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h +++ b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h @@ -612,6 +612,7 @@ enum class Opcode { FLAT_LOAD_USHORT, FLAT_LOAD_SSHORT, FLAT_LOAD_SHORT_D16, + FLAT_LOAD_SHORT_D16_HI, FLAT_LOAD_DWORD, FLAT_LOAD_DWORDX2, FLAT_LOAD_DWORDX3, diff --git a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp index 73993799c..bfc99bc3b 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp @@ -1146,6 +1146,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) { case Decoder::Opcode::FLAT_LOAD_USHORT: case Decoder::Opcode::FLAT_LOAD_SSHORT: case Decoder::Opcode::FLAT_LOAD_SHORT_D16: + case Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI: case Decoder::Opcode::FLAT_LOAD_DWORD: case Decoder::Opcode::FLAT_LOAD_DWORDX2: case Decoder::Opcode::FLAT_LOAD_DWORDX3: diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index a6a64f143..f43577426 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -22667,6 +22667,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode, case Opcode::FLAT_LOAD_USHORT: case Opcode::FLAT_LOAD_SSHORT: case Opcode::FLAT_LOAD_SHORT_D16: + case Opcode::FLAT_LOAD_SHORT_D16_HI: case Opcode::FLAT_LOAD_DWORD: case Opcode::FLAT_LOAD_DWORDX2: case Opcode::FLAT_LOAD_DWORDX3: @@ -35765,7 +35766,17 @@ TestCase FlatSubdwordLoadsApplyByteOffset() { return test; } -TestCase GlobalLoadShortD16Captured(u32 wave_size) { +// D16_HI loads move the raw halfword into bits [31:16] and keep the old low half. +constexpr u32 MergeD16Hi(u32 old_value, u32 halfword) { + return (old_value & 0xffffu) | ((halfword & 0xffffu) << 16u); +} + +static_assert(MergeD16Hi(0xa5a51234u, 0x0000u) == 0x00001234u && + MergeD16Hi(0xa5a51234u, 0x7fffu) == 0x7fff1234u && + MergeD16Hi(0xa5a51234u, 0x8000u) == 0x80001234u && + MergeD16Hi(0xa5a51234u, 0xffffu) == 0xffff1234u); + +TestCase GlobalLoadShortD16Captured(u32 wave_size, bool high = false) { using O = ShaderOpcode; constexpr uint64_t GuestBase = 0x0000000110000000ull; constexpr u32 Marker = 0xa5a50000u; @@ -35773,16 +35784,32 @@ TestCase GlobalLoadShortD16Captured(u32 wave_size) { constexpr uint64_t ExecMask = 0xaaaaaaaa55555555ull; constexpr u32 Values[] = {0u, 0x7fffu, 0x8000u, 0xffffu}; TestCase test; - test.name = wave_size == 32u ? "GlobalLoadShortD16CapturedWave32" - : "GlobalLoadShortD16CapturedWave64"; - test.initial.resize(2u * wave_size + wave_size / 2u); - test.expected.resize(2u * wave_size); + if (high) { + test.name = wave_size == 32u ? "GlobalLoadShortD16HiCapturedWave32" + : "GlobalLoadShortD16HiCapturedWave64"; + } else { + test.name = wave_size == 32u ? "GlobalLoadShortD16CapturedWave32" + : "GlobalLoadShortD16CapturedWave64"; + } + // Pass 0 runs with the full EXEC, pass 1 with a partial EXEC and pass 2 (high half only) with + // EXEC=0 and an unmapped address, which must neither touch memory nor change the destination. + // Pass 2 reads its zero EXEC from the buffer so that the guard is exercised at run time rather + // than folded away together with the load. + const u32 passes = high ? 3u : 2u; + const u32 zero_dword = passes * wave_size + wave_size / 2u; + test.initial.resize(zero_dword + (high ? 1u : 0u)); + test.expected.resize(passes * wave_size); for (u32 lane = 0; lane < wave_size; ++lane) { const u32 value = Values[lane % std::size(Values)]; - test.initial[2u * wave_size + lane / 2u] |= value << (16u * (lane % 2u)); - test.expected[lane] = Marker | value; - test.expected[wave_size + lane] = Marker | - ((ExecMask & (uint64_t{1} << lane)) != 0u ? value : lane * 2u); + test.initial[passes * wave_size + lane / 2u] |= value << (16u * (lane % 2u)); + for (u32 pass = 0; pass < passes; ++pass) { + const bool active = + pass == 0 || (pass == 1 && (ExecMask & (uint64_t{1} << lane)) != 0); + const u32 old_value = Marker | (lane * 2u); + test.expected[pass * wave_size + lane] = + !active ? old_value + : (high ? MergeD16Hi(old_value, value) : Marker | value); + } } auto &code = test.code; AppendSMovLiteral(&code, 2, static_cast(ScalarBase)); @@ -35790,20 +35817,35 @@ TestCase GlobalLoadShortD16Captured(u32 wave_size) { code.push_back(EncodeVop2(0x1a, 6, InlineU32(1), 0)); AppendVMovLiteral(&code, 5, Marker); code.push_back(EncodeVop2(0x25, 6, Vgpr(5), 6)); - for (bool masked : {false, true}) { + for (u32 pass = 0; pass < passes; ++pass) { + const bool masked = pass != 0; code.push_back(EncodeVop1(0x01, 5, Vgpr(6))); if (masked) { code.push_back(EncodeSop1(0x04, 8, 126)); - AppendSMovLiteral(&code, 126, static_cast(ExecMask)); - AppendSMovLiteral(&code, 127, static_cast(ExecMask >> 32u)); + if (pass == 1) { + AppendSMovLiteral(&code, 126, static_cast(ExecMask)); + AppendSMovLiteral(&code, 127, static_cast(ExecMask >> 32u)); + } else { + // s[2:3] hold the scalar base, so read through the output buffer descriptor s[48:51]. + AppendVMovU32(&code, 9, zero_dword * 4u); + code.push_back(EncodeMubuf0(0x0cu)); + code.push_back(EncodeMubuf1(9, 12, 9)); + code.push_back(EncodeVop1(0x02, 20, Vgpr(9))); + code.push_back(EncodeVop1(0x02, 21, Vgpr(9))); + code.push_back(EncodeSop1(0x04, 126, 20)); + AppendSMovLiteral(&code, 2, 0u); + AppendSMovLiteral(&code, 3, 0u); + } } - // Captured GLOBAL_LOAD_SHORT_D16: v5 supplies the byte offset and destination. - code.insert(code.end(), {0xdc908070u, 0x05020005u}); + // Captured GLOBAL_LOAD_SHORT_D16 (the high variant only switches the opcode to D16_HI): + // v5 supplies the byte offset and destination. + code.insert(code.end(), {high ? 0xdc948070u : 0xdc908070u, 0x05020005u}); if (masked) code.push_back(EncodeSop1(0x04, 126, 8)); - AppendStoreVgprAtLaneDwordOffset(&code, 5, 0, masked ? wave_size : 0u); + AppendStoreVgprAtLaneDwordOffset(&code, 5, 0, pass * wave_size); } AppendEnd(&code); - test.bda_mappings = {{GuestBase, 8u * wave_size}}; + test.bda_mappings = {{GuestBase, 4u * passes * wave_size}}; + if (high) test.fault_free_address = uint64_t{Marker} + 0x70u; test.compute_info.threads_num[0] = wave_size; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; @@ -35811,13 +35853,21 @@ TestCase GlobalLoadShortD16Captured(u32 wave_size) { test.compute_info.wave_size = wave_size; test.has_compute_info = true; test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::V_MOV_B32, O::V_LSHLREV_B32, - O::V_ADD_NC_U32, O::FLAT_LOAD_SHORT_D16, + O::V_ADD_NC_U32, + high ? O::FLAT_LOAD_SHORT_D16_HI : O::FLAT_LOAD_SHORT_D16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; - test.decoded_counts = {{"FLAT_LOAD_SHORT_D16", 2}}; + test.decoded_counts = { + {high ? "FLAT_LOAD_SHORT_D16_HI" : "FLAT_LOAD_SHORT_D16", passes}}; + if (high) { + test.opcodes.insert(test.opcodes.end(), + {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32}); + // Every pass, including the EXEC=0 one, must keep its load. + test.ir_counts = {{"LoadAddressU16", passes}}; + } return test; } -TestCase FlatLoadShortD16AddressSegments() { +TestCase FlatLoadShortD16AddressSegments(bool high = false) { using O = ShaderOpcode; constexpr uint64_t GuestBase = 0x0000000110000000ull; std::vector code; @@ -35825,7 +35875,8 @@ TestCase FlatLoadShortD16AddressSegments() { AppendVMovLiteral(&code, 21, static_cast(GuestBase >> 32u)); for (u32 segment : {0u, 2u}) { AppendVMovLiteral(&code, 3, 0x12345678u); - code.push_back(EncodeFlat0(0x24, segment, segment == 0u ? 0u : 0xffeu)); + code.push_back(EncodeFlat0(high ? 0x25 : 0x24, segment, + segment == 0u ? 0u : 0xffeu)); code.push_back(EncodeFlat1(3, 0x7d, 0, 20)); AppendStoreVgpr(&code, 3, segment == 0u ? 0u : 1u); } @@ -35834,20 +35885,121 @@ TestCase FlatLoadShortD16AddressSegments() { code.push_back(EncodeFlat0(0x1c, 1, 0)); code.push_back(EncodeFlat1(0, 0x7d, 3, 20)); AppendVMovLiteral(&code, 3, 0x98761234u); - code.push_back(EncodeFlat0(0x24, 1, 2)); + code.push_back(EncodeFlat0(high ? 0x25 : 0x24, 1, 2)); code.push_back(EncodeFlat1(3, 0x7d, 0, 20)); AppendStoreVgpr(&code, 3, 2); AppendEnd(&code); - TestCase test{"FlatLoadShortD16AddressSegments", code, + TestCase test{high ? "FlatLoadShortD16HiAddressSegments" + : "FlatLoadShortD16AddressSegments", + code, {0, 0, 0, 0, 0x80017fffu}, - {0x12348001u, 0x12347fffu, 0x9876beefu}, - {O::V_MOV_B32, O::FLAT_LOAD_SHORT_D16, O::FLAT_STORE_DWORD, - O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; + high ? std::vector{0x80015678u, 0x7fff5678u, 0xbeef1234u} + : std::vector{0x12348001u, 0x12347fffu, 0x9876beefu}, + {O::V_MOV_B32, + high ? O::FLAT_LOAD_SHORT_D16_HI : O::FLAT_LOAD_SHORT_D16, + O::FLAT_STORE_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.bda_mappings = {{GuestBase, 16}}; test.compute_info.scratch_size_dwords = 1; return test; } +TestCase GlobalLoadShortD16HiMergesOldDestination(u32 wave_size) { + using O = ShaderOpcode; + constexpr uint64_t GuestBase = 0x0000000110000000ull; + constexpr uint64_t ScalarBase = GuestBase - 196u; + constexpr u32 Values[] = {0u, 0x7fffu, 0x8000u, 0xffffu}; + // The destination does not alias the address. Changing only the old high half must not change + // the result, and changing only the old low half must change only the low half of the result. + constexpr u32 OldValues[] = {0xa5a51234u, 0x5a5a1234u, 0xa5a5abcdu}; + constexpr u32 Neighbor = 0x0badf00du; + constexpr u32 Passes = static_cast(std::size(OldValues)); + TestCase test; + test.name = wave_size == 32u ? "GlobalLoadShortD16HiMergesOldDestinationWave32" + : "GlobalLoadShortD16HiMergesOldDestinationWave64"; + const u32 data_dword = (Passes + 1u) * wave_size; + test.initial.resize(data_dword + wave_size / 2u); + test.expected.resize(data_dword); + for (u32 lane = 0; lane < wave_size; ++lane) { + const u32 value = Values[lane % std::size(Values)]; + test.initial[data_dword + lane / 2u] |= value << (16u * (lane % 2u)); + for (u32 pass = 0; pass < Passes; ++pass) { + test.expected[pass * wave_size + lane] = MergeD16Hi(OldValues[pass], value); + } + test.expected[Passes * wave_size + lane] = Neighbor; + } + auto &code = test.code; + AppendSMovLiteral(&code, 0, static_cast(ScalarBase)); + AppendSMovLiteral(&code, 1, static_cast(ScalarBase >> 32u)); + code.push_back(EncodeVop2(0x1a, 18, InlineU32(1), 0)); + AppendVMovLiteral(&code, 17, Neighbor); + for (u32 pass = 0; pass < Passes; ++pass) { + AppendVMovLiteral(&code, 16, OldValues[pass]); + // Captured from Ghost of Yotei CS 0x79b0066f0cd1f3cd: + // global_load_short_d16_hi v16, v18, s[0:1] offset:196 + code.insert(code.end(), {0xdc9480c4u, 0x10000012u}); + AppendStoreVgprAtLaneDwordOffset(&code, 16, 0, pass * wave_size); + } + AppendStoreVgprAtLaneDwordOffset(&code, 17, 0, Passes * wave_size); + AppendEnd(&code); + test.bda_mappings = {{GuestBase, 4u * data_dword}}; + test.compute_info.threads_num[0] = wave_size; + test.compute_info.threads_num[1] = 1; + test.compute_info.threads_num[2] = 1; + test.compute_info.thread_ids_num = 1; + test.compute_info.wave_size = wave_size; + test.has_compute_info = true; + test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, + O::V_LSHLREV_B32, O::V_ADD_NC_U32, + O::FLAT_LOAD_SHORT_D16_HI, O::BUFFER_STORE_DWORD, + O::S_ENDPGM}; + test.decoded_counts = {{"FLAT_LOAD_SHORT_D16_HI", Passes}}; + return test; +} + +TestCase FlatLoadShortD16HiIntoAddressHighVgpr(u32 wave_size) { + using O = ShaderOpcode; + constexpr uint64_t GuestBase = 0x0000000110000000ull; + constexpr u32 Values[] = {0u, 0x7fffu, 0x8000u, 0xffffu}; + constexpr u32 AddressHigh = static_cast(GuestBase >> 32u); + TestCase test; + test.name = wave_size == 32u ? "FlatLoadShortD16HiIntoAddressHighVgprWave32" + : "FlatLoadShortD16HiIntoAddressHighVgprWave64"; + const u32 data_dword = 2u * wave_size; + test.initial.resize(data_dword + wave_size / 2u); + test.expected.resize(data_dword); + for (u32 lane = 0; lane < wave_size; ++lane) { + const u32 value = Values[lane % std::size(Values)]; + test.initial[data_dword + lane / 2u] |= value << (16u * (lane % 2u)); + // The 64-bit address is read before v4 is replaced, so the old high address dword supplies + // the preserved low half and the low address dword stays as it was. + test.expected[lane] = MergeD16Hi(AddressHigh, value); + test.expected[wave_size + lane] = static_cast(GuestBase) + lane * 2u; + } + auto &code = test.code; + code.push_back(EncodeVop2(0x1a, 3, InlineU32(1), 0)); + AppendVMovLiteral(&code, 9, static_cast(GuestBase)); + code.push_back(EncodeVop2(0x25, 3, Vgpr(9), 3)); + AppendVMovLiteral(&code, 4, AddressHigh); + // flat_load_short_d16_hi v4, v[3:4] + code.push_back(EncodeFlat0(0x25, 0, 0)); + code.push_back(EncodeFlat1(4, 0x7d, 0, 3)); + AppendStoreVgprAtLaneDwordOffset(&code, 4, 0, 0); + AppendStoreVgprAtLaneDwordOffset(&code, 3, 0, wave_size); + AppendEnd(&code); + test.bda_mappings = {{GuestBase, 4u * data_dword}}; + test.compute_info.threads_num[0] = wave_size; + test.compute_info.threads_num[1] = 1; + test.compute_info.threads_num[2] = 1; + test.compute_info.thread_ids_num = 1; + test.compute_info.wave_size = wave_size; + test.has_compute_info = true; + test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, + O::V_ADD_NC_U32, O::FLAT_LOAD_SHORT_D16_HI, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.decoded_counts = {{"FLAT_LOAD_SHORT_D16_HI", 1}}; + return test; +} + TestCase BranchVccnzUsesCarryProducedWaveMask() { using O = ShaderOpcode; @@ -45002,7 +45154,14 @@ std::vector MakeCases() { AddCase(FlatSubdwordLoadsApplyByteOffset); cases.push_back(GlobalLoadShortD16Captured(32)); cases.push_back(GlobalLoadShortD16Captured(64)); - AddCase(FlatLoadShortD16AddressSegments); + cases.push_back(GlobalLoadShortD16Captured(32, true)); + cases.push_back(GlobalLoadShortD16Captured(64, true)); + cases.push_back(FlatLoadShortD16AddressSegments()); + cases.push_back(FlatLoadShortD16AddressSegments(true)); + cases.push_back(GlobalLoadShortD16HiMergesOldDestination(32)); + cases.push_back(GlobalLoadShortD16HiMergesOldDestination(64)); + cases.push_back(FlatLoadShortD16HiIntoAddressHighVgpr(32)); + cases.push_back(FlatLoadShortD16HiIntoAddressHighVgpr(64)); AddCase(FlatVirtualAddressRebasesGuestAllocation); AddCase(GlobalSignedImmediateRebasesBeforeSaddr); AddCase(FlatSegmentIgnoresSaddrAndMasksOffsetMsb); @@ -51501,7 +51660,14 @@ int main(int argc, char **argv) { RunCase(&vulkan, FlatLoadDlcCaptured(64)); RunCase(&vulkan, GlobalLoadShortD16Captured(32)); RunCase(&vulkan, GlobalLoadShortD16Captured(64)); + RunCase(&vulkan, GlobalLoadShortD16Captured(32, true)); + RunCase(&vulkan, GlobalLoadShortD16Captured(64, true)); RunCase(&vulkan, FlatLoadShortD16AddressSegments()); + RunCase(&vulkan, FlatLoadShortD16AddressSegments(true)); + for (const u32 wave : {32u, 64u}) { + RunCase(&vulkan, GlobalLoadShortD16HiMergesOldDestination(wave)); + RunCase(&vulkan, FlatLoadShortD16HiIntoAddressHighVgpr(wave)); + } RunCase(&vulkan, FlatSubdwordLoadsApplyByteOffset()); RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation()); RunCase(&vulkan, GlobalSignedImmediateRebasesBeforeSaddr()); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 25c2bde72..c773fd8d9 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -16130,6 +16130,153 @@ void TestCapturedBufferLoadUbyteD16Decode() { "captured BUFFER_LOAD_UBYTE_D16 lost its preserved half, address, or coherence flags"); } +void TestCapturedGlobalLoadShortD16HiDecode() { + namespace Decoder = ShaderRecompiler::Decoder; + // Ghost of Yotei CS 0x79b0066f0cd1f3cd: global_load_short_d16_hi v16, v18, s[0:1] offset:196. + const uint32_t code[] = {0xdc9480c4u, 0x10000012u}; + Decoder::Instruction inst; + Decoder::DecodeInstruction(code, 0, inst); + Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && + inst.family == Decoder::Family::FLAT && inst.word_count == 2 && + inst.data_dwords == 1 && inst.data_bits == 16 && !inst.data_signed && + inst.memory_segment == 2 && inst.offset == 196 && !inst.glc && !inst.slc && + !inst.dlc, + "captured GLOBAL_LOAD_SHORT_D16_HI lost its halfword payload or address fields"); + Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 16 && + inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2, + "captured GLOBAL_LOAD_SHORT_D16_HI must write only the high half of v16"); + Check(inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 18 && + inst.src1.kind == Decoder::OperandKind::Sgpr && inst.src1.reg == 0, + "captured GLOBAL_LOAD_SHORT_D16_HI lost its VGPR offset or s[0:1] base"); + Check(Decoder::InstructionToString(inst).find("FLAT_LOAD_SHORT_D16_HI") != std::string::npos, + "captured GLOBAL_LOAD_SHORT_D16_HI diagnostic text lost the opcode name"); +} + +void TestFlatLoadShortD16HiLlvmEncodingAnchor() { + namespace Decoder = ShaderRecompiler::Decoder; + // llvm/test/MC/AMDGPU/flat-gfx10.s: flat_load_short_d16_hi v1, v[3:4] + // encodes as [0x00,0x00,0x94,0xdc,0x03,0x00,0x7d,0x01]. + const uint32_t code[] = {0xdc940000u, 0x017d0003u}; + Decoder::Instruction inst; + Decoder::DecodeInstruction(code, 0, inst); + Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && inst.word_count == 2 && + inst.memory_segment == 0 && inst.offset == 0 && inst.data_dwords == 1 && + inst.data_bits == 16 && !inst.data_signed, + "LLVM flat_load_short_d16_hi encoding did not decode as a FLAT halfword load"); + Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 1 && + inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2 && + inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 3 && + inst.src1.kind == Decoder::OperandKind::Vgpr && inst.src1.reg == 4, + "LLVM flat_load_short_d16_hi v1, v[3:4] lost its destination half or address pair"); +} + +void TestFlatLoadShortD16HiCompilesAndValidates() { + // The captured high-half load followed by a buffer store of the merged register so that the + // load stays live: global_load_short_d16_hi v16, v18, s[0:1] offset:196. + const uint32_t shader[] = { + 0xdc9480c4u, 0x10000012u, + EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(16, 12, 0), + EncodeSopp(0x01), + }; + auto options = MakeCompileOptions(ShaderType::Compute); + options.dump_ir = true; + auto result = RecompileForTest(shader, options); + Check(result.decoded_dump.find("FLAT_LOAD_SHORT_D16_HI") != std::string::npos && + result.decoded_dump.find("unsupported family=") == std::string::npos, + "GLOBAL_LOAD_SHORT_D16_HI did not reach the decoded program"); + Check(result.ir_dump.find("LoadAddressU16") != std::string::npos && + result.ir_dump.find("BitFieldInsert") != std::string::npos, + "GLOBAL_LOAD_SHORT_D16_HI did not become a 16-bit load merged into the old register"); + CheckSpirvBinaryValidates(result.spirv); +} + +void TestFlatLoadShortD16HiAddressFields() { + namespace Decoder = ShaderRecompiler::Decoder; + // Modifier bits of the first FLAT-family word. + constexpr uint32_t DlcBit = 1u << 12u; + constexpr uint32_t LdsBit = 1u << 13u; + constexpr uint32_t GlcBit = 1u << 16u; + constexpr uint32_t SlcBit = 1u << 17u; + constexpr uint32_t SegFlat = 0; + constexpr uint32_t SegScratch = 1; + constexpr uint32_t SegGlobal = 2; + constexpr uint32_t SegReserved = 3; + constexpr uint32_t SaddrOff = 0x7du; + const auto decode = [](uint32_t seg, uint32_t offset_field, uint32_t saddr, uint32_t flags = 0) { + const uint32_t code[] = {EncodeFlat0(0x25, seg, offset_field) | flags, + EncodeFlat1(7, saddr, 0, 9)}; + Decoder::Instruction inst; + Decoder::DecodeInstruction(code, 0, inst); + return inst; + }; + + struct OffsetCase { + uint32_t seg; + uint32_t field; + int32_t expected; + }; + const OffsetCase offsets[] = { + // FLAT ignores OFFSET[11] and uses an unsigned 11-bit byte offset. + {SegFlat, 0x000, 0}, + {SegFlat, 0x7ff, 2047}, + {SegFlat, 0x800, 0}, + {SegFlat, 0xfff, 2047}, + // GLOBAL and SCRATCH sign-extend the 12-bit field. + {SegGlobal, 0x000, 0}, + {SegGlobal, 0x7ff, 2047}, + {SegGlobal, 0x800, -2048}, + {SegGlobal, 0xfff, -1}, + {SegScratch, 0x000, 0}, + {SegScratch, 0x7ff, 2047}, + {SegScratch, 0x800, -2048}, + {SegScratch, 0xfff, -1}, + }; + for (const auto &test : offsets) { + const auto inst = decode(test.seg, test.field, SaddrOff); + Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && + inst.memory_segment == test.seg && + inst.offset == static_cast(test.expected), + "FLAT-family D16 high-half load decoded the wrong byte offset for its segment"); + Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 7 && + inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2 && + inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 9, + "FLAT-family D16 high-half load lost its destination half or address VGPR"); + } + + // FLAT takes a VGPR pair whatever the SADDR field says; GLOBAL takes either an SGPR base plus + // a VGPR offset or, with SADDR off, a VGPR pair. + const auto flat = decode(SegFlat, 0, 2); + Check(flat.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && + flat.src1.kind == Decoder::OperandKind::Vgpr && flat.src1.reg == 10, + "FLAT D16 high-half load must use the VGPR pair and ignore SADDR"); + const auto global_sgpr = decode(SegGlobal, 0, 2); + Check(global_sgpr.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && + global_sgpr.src_count == 2 && global_sgpr.src0.kind == Decoder::OperandKind::Vgpr && + global_sgpr.src0.reg == 9 && global_sgpr.src1.kind == Decoder::OperandKind::Sgpr && + global_sgpr.src1.reg == 2, + "GLOBAL D16 high-half load with SADDR s[2:3] lost its scalar base"); + const auto global_pair = decode(SegGlobal, 0, SaddrOff); + Check(global_pair.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && + global_pair.src_count == 2 && global_pair.src0.kind == Decoder::OperandKind::Vgpr && + global_pair.src0.reg == 9 && global_pair.src1.kind == Decoder::OperandKind::Vgpr && + global_pair.src1.reg == 10, + "GLOBAL D16 high-half load with SADDR off must use the VGPR pair"); + + // DLC is only a cache hint and stays visible; LDS, GLC and SLC on a load and the reserved + // segment are not implemented and must keep failing loudly. + const auto dlc = decode(SegGlobal, 4, 2, DlcBit); + Check(dlc.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && dlc.dlc && !dlc.glc && !dlc.slc, + "GLOBAL D16 high-half load lost its DLC hint"); + Check(decode(SegGlobal, 0, 2, LdsBit).opcode == Decoder::Opcode::UNSUPPORTED, + "FLAT-family D16 high-half load accepted LDS=1"); + Check(decode(SegGlobal, 0, 2, GlcBit).opcode == Decoder::Opcode::UNSUPPORTED, + "FLAT-family D16 high-half load accepted GLC=1"); + Check(decode(SegGlobal, 0, 2, SlcBit).opcode == Decoder::Opcode::UNSUPPORTED, + "FLAT-family D16 high-half load accepted SLC=1 on a load"); + Check(decode(SegReserved, 0, 2).opcode == Decoder::Opcode::UNSUPPORTED, + "FLAT-family D16 high-half load accepted the reserved segment"); +} + void TestCapturedBufferStoreShortD16HiDecode() { namespace Decoder = ShaderRecompiler::Decoder; const uint32_t code[] = {0xe06c6000u, 0x80010326u}; @@ -17017,6 +17164,10 @@ int main() { TestPixelProgramCacheBindingIdentity(); TestGraphicsPushConstantPlacement(); TestCapturedBufferLoadUbyteD16Decode(); + TestCapturedGlobalLoadShortD16HiDecode(); + TestFlatLoadShortD16HiLlvmEncodingAnchor(); + TestFlatLoadShortD16HiAddressFields(); + TestFlatLoadShortD16HiCompilesAndValidates(); TestCapturedBufferStoreShortD16HiDecode(); TestCapturedBufferStoreFormatD16HiXDecode(); TestCapturedBufferLoadD16XyDecode();