From c0519cc8145442c1c84aa843553749bbacd3bca5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 14:59:34 +0000 Subject: [PATCH] feat(shader): implement the 32-bit DS src2 operations The decoder threw on the GFX10.1 DS src2 opcodes 0x80-0x8b, 0x8d, 0x92, 0x93 and 0x95. Each reads DS[B], where B is the address plus four times the 15-bit signed offset in offset1[6:0]:offset0, and applies the matching operation to DS[A]: add, sub, rsub, inc, dec, signed and unsigned min and max, and, or, xor, f32 min, max and add through the 32-bit LDS atomics, and a plain write for ds_write_src2_b32. The form that takes the offset from the address bits (offset1[7]), the 64-bit forms and GDS still throw. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LdsSrc2.cpp | 152 ++++++++++++++++++ .../include/RdnaDecoder/RdnaOpcode.hpp | 16 ++ .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 45 ++++++ .../Translation/TranslationContext.hpp | 1 + .../MemoryInstructionDispatch.cpp | 32 ++++ .../SharedMemoryInstructions.cpp | 17 ++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 264 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LdsSrc2.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsSrc2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsSrc2.cpp new file mode 100644 index 000000000..e7dd4fea7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsSrc2.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000201, 0x340c0082, 0xbf8c3f70, 0x4a0e0cff, 0x00001000, 0x4a100c80, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda007c00, 0x00000007, 0xd8d80000, 0x0a000007, + 0x4a0e0cff, 0x00001080, 0x4a100cff, 0x00002080, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, + 0xda040400, 0x00000007, 0xd8d80000, 0x0b000007, 0x4a0e0cff, 0x00001100, 0x4a100cff, 0x00000100, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda087c00, 0x00000007, 0xd8d80000, 0x0c000007, + 0x4a0e0cff, 0x00001180, 0x4a100cff, 0x00002180, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, + 0xda0c0400, 0x00000007, 0xd8d80000, 0x0d000007, 0x4a0e0cff, 0x00001200, 0x4a100cff, 0x00000200, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda107c00, 0x00000007, 0xd8d80000, 0x0e000007, + 0x4a0e0cff, 0x00001280, 0x4a100cff, 0x00002280, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, + 0xda140400, 0x00000007, 0xd8d80000, 0x0f000007, 0x4a0e0cff, 0x00001300, 0x4a100cff, 0x00000300, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda187c00, 0x00000007, 0xd8d80000, 0x10000007, + 0x4a0e0cff, 0x00001380, 0x4a100cff, 0x00002380, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, + 0xda1c0400, 0x00000007, 0xd8d80000, 0x11000007, 0x4a0e0cff, 0x00001400, 0x4a100cff, 0x00000400, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda207c00, 0x00000007, 0xd8d80000, 0x12000007, + 0x4a0e0cff, 0x00001480, 0x4a100cff, 0x00002480, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, + 0xda240400, 0x00000007, 0xd8d80000, 0x13000007, 0x4a0e0cff, 0x00001500, 0x4a100cff, 0x00000500, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda287c00, 0x00000007, 0xd8d80000, 0x14000007, + 0x4a0e0cff, 0x00001580, 0x4a100cff, 0x00002580, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, + 0xda2c0400, 0x00000007, 0xd8d80000, 0x15000007, 0x4a0e0cff, 0x00001600, 0x4a100cff, 0x00000600, + 0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda347c00, 0x00000007, 0xd8d80000, 0x16000007, + 0x4a0e0cff, 0x00001680, 0x4a100cff, 0x00002680, 0xd8340000, 0x00000407, 0xd8340000, 0x00000508, + 0xda480400, 0x00000007, 0xd8d80000, 0x17000007, 0x4a0e0cff, 0x00001700, 0x4a100cff, 0x00000700, + 0xd8340000, 0x00000407, 0xd8340000, 0x00000508, 0xda4c7c00, 0x00000007, 0xd8d80000, 0x18000007, + 0x4a0e0cff, 0x00001780, 0x4a100cff, 0x00002780, 0xd8340000, 0x00000407, 0xd8340000, 0x00000508, + 0xda540400, 0x00000007, 0xd8d80000, 0x19000007, 0x34020086, 0xbf8cc07f, 0xe0781000, 0x80010a01, + 0xe0781010, 0x80010e01, 0xe0781020, 0x80011201, 0xe0781030, 0x80011601, 0xbf810000, +}; + +alignas(256) constexpr std::array AddressOffsetCode{0xda008000, 0x00000000, 0xbf810000}; + +void Fill(std::uint32_t tid, std::uint32_t* words) { + const std::uint64_t seed = (tid + 1u) * 0x9e3779b97f4a7c15ull; + const auto mixed = seed * 0xbf58476d1ce4e5b9ull; + words[0] = tid % 4u == 0u ? tid % 7u : static_cast(seed); + words[1] = tid % 4u == 0u ? (tid * 3u) % 7u : tid % 4u == 1u ? words[0] : static_cast(mixed >> 32u); + words[2] = std::bit_cast(static_cast(static_cast(tid % 9u) - 4) * 1.75f + 0.5f); + words[3] = std::bit_cast(static_cast(static_cast((tid * 5u) % 11u) - 5) * 0.625f + 0.25f); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 10240u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + const auto result = Compile(device, Code); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +void CheckLanes() { + constexpr std::array names{ + "ds_add_src2_u32", "ds_sub_src2_u32", "ds_rsub_src2_u32", "ds_inc_src2_u32", "ds_dec_src2_u32", "ds_min_src2_i32", "ds_max_src2_i32", "ds_min_src2_u32", + "ds_max_src2_u32", "ds_and_src2_b32", "ds_or_src2_b32", "ds_xor_src2_b32", "ds_write_src2_b32", "ds_min_src2_f32", "ds_max_src2_f32", "ds_add_src2_f32", + }; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t a = in[0]; + const std::uint32_t b = in[1]; + const auto sa = static_cast(a); + const auto sb = static_cast(b); + const float fa = std::bit_cast(in[2]); + const float fb = std::bit_cast(in[3]); + const std::array expected{ + a + b, a - b, b - a, a >= b ? 0u : a + 1u, (a == 0u || a > b) ? b : a - 1u, + static_cast(std::min(sa, sb)), static_cast(std::max(sa, sb)), std::min(a, b), std::max(a, b), + a & b, a | b, a ^ b, b, + std::bit_cast(std::min(fa, fb)), std::bit_cast(std::max(fa, fb)), std::bit_cast(fa + fb), + }; + for (std::uint32_t op = 0; op < Results; ++op) { + const std::uint32_t actual = Output[tid * Results + op]; + Require(actual == expected[op], "lds src2: lane " + std::to_string(tid) + " " + names[op] + " is " + Hex(actual) + ", expected " + Hex(expected[op])); + } + } +} + +void CheckAddressOffsetRejected(AgcDriver::VulkanDevice& device) { + try { + (void)Compile(device, AddressOffsetCode); + } catch (const std::exception&) { + return; + } + Require(false, "lds src2: an offset taken from the address was accepted"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + CheckLanes(); + CheckAddressOffsetRejected(*device); + std::puts("lds src2 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index 3be783c0c..447cf0593 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -938,6 +938,22 @@ enum class RdnaOpcode : std::uint16_t { DsCmpstRtnF64, DsMinRtnF64, DsMaxRtnF64, + DsAddSrc2U32, + DsSubSrc2U32, + DsRsubSrc2U32, + DsIncSrc2U32, + DsDecSrc2U32, + DsMinSrc2I32, + DsMaxSrc2I32, + DsMinSrc2U32, + DsMaxSrc2U32, + DsAndSrc2B32, + DsOrSrc2B32, + DsXorSrc2B32, + DsWriteSrc2B32, + DsMinSrc2F32, + DsMaxSrc2F32, + DsAddSrc2F32, DsNop, DsReadU8D16, DsReadU8D16Hi, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 5bc29e580..8f8c5c142 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -283,6 +283,22 @@ constexpr MemoryOpcodeInfo dsOpcodes[] = { {0x71u, RdnaOpcode::DsCmpstRtnF64, 2, 32, false, false, false}, {0x72u, RdnaOpcode::DsMinRtnF64, 2, 32, false, false, false}, {0x73u, RdnaOpcode::DsMaxRtnF64, 2, 32, false, false, false}, + {0x80u, RdnaOpcode::DsAddSrc2U32, 1, 32, false, false, false}, + {0x81u, RdnaOpcode::DsSubSrc2U32, 1, 32, false, false, false}, + {0x82u, RdnaOpcode::DsRsubSrc2U32, 1, 32, false, false, false}, + {0x83u, RdnaOpcode::DsIncSrc2U32, 1, 32, false, false, false}, + {0x84u, RdnaOpcode::DsDecSrc2U32, 1, 32, false, false, false}, + {0x85u, RdnaOpcode::DsMinSrc2I32, 1, 32, false, false, false}, + {0x86u, RdnaOpcode::DsMaxSrc2I32, 1, 32, false, false, false}, + {0x87u, RdnaOpcode::DsMinSrc2U32, 1, 32, false, false, false}, + {0x88u, RdnaOpcode::DsMaxSrc2U32, 1, 32, false, false, false}, + {0x89u, RdnaOpcode::DsAndSrc2B32, 1, 32, false, false, false}, + {0x8au, RdnaOpcode::DsOrSrc2B32, 1, 32, false, false, false}, + {0x8bu, RdnaOpcode::DsXorSrc2B32, 1, 32, false, false, false}, + {0x8du, RdnaOpcode::DsWriteSrc2B32, 1, 32, false, false, false}, + {0x92u, RdnaOpcode::DsMinSrc2F32, 1, 32, false, false, false}, + {0x93u, RdnaOpcode::DsMaxSrc2F32, 1, 32, false, false, false}, + {0x95u, RdnaOpcode::DsAddSrc2F32, 1, 32, false, false, false}, {0x01u, RdnaOpcode::DsSubU32, 1, 32, false, false, false}, {0x05u, RdnaOpcode::DsMinI32, 1, 32, false, false, false}, {0x06u, RdnaOpcode::DsMaxI32, 1, 32, false, false, false}, @@ -554,6 +570,29 @@ bool isDsAtomicOpcode(RdnaOpcode opcode) { } } +bool isDsSrc2Opcode(RdnaOpcode opcode) { + switch (opcode) { + case RdnaOpcode::DsAddSrc2U32: + case RdnaOpcode::DsSubSrc2U32: + case RdnaOpcode::DsRsubSrc2U32: + case RdnaOpcode::DsIncSrc2U32: + case RdnaOpcode::DsDecSrc2U32: + case RdnaOpcode::DsMinSrc2I32: + case RdnaOpcode::DsMaxSrc2I32: + case RdnaOpcode::DsMinSrc2U32: + case RdnaOpcode::DsMaxSrc2U32: + case RdnaOpcode::DsAndSrc2B32: + case RdnaOpcode::DsOrSrc2B32: + case RdnaOpcode::DsXorSrc2B32: + case RdnaOpcode::DsWriteSrc2B32: + case RdnaOpcode::DsMinSrc2F32: + case RdnaOpcode::DsMaxSrc2F32: + case RdnaOpcode::DsAddSrc2F32: + return true; + default: return false; + } +} + std::uint32_t dsSourceCount(RdnaOpcode opcode) { switch (opcode) { case RdnaOpcode::DsWrxchg2RtnB32: @@ -975,6 +1014,12 @@ RdnaInstruction DecodeRdnaDs(std::uint32_t programCounter, std::span::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation.