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 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-10-06 14:59:34 +00:00 committed by Adrià Franch
parent 0447793216
commit c0519cc814
7 changed files with 264 additions and 0 deletions

View File

@ -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 <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>
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<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
alignas(256) constexpr std::array<std::uint32_t, 207> 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<std::uint32_t, 3> 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<std::uint32_t>(seed);
words[1] = tid % 4u == 0u ? (tid * 3u) % 7u : tid % 4u == 1u ? words[0] : static_cast<std::uint32_t>(mixed >> 32u);
words[2] = std::bit_cast<std::uint32_t>(static_cast<float>(static_cast<std::int32_t>(tid % 9u) - 4) * 1.75f + 0.5f);
words[3] = std::bit_cast<std::uint32_t>(static_cast<float>(static_cast<std::int32_t>((tid * 5u) % 11u) - 5) * 0.625f + 0.25f);
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((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<const std::uint32_t> code) {
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(Code.data()));
device.WaitIdle();
}
void CheckLanes() {
constexpr std::array<const char*, Results> 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<std::int32_t>(a);
const auto sb = static_cast<std::int32_t>(b);
const float fa = std::bit_cast<float>(in[2]);
const float fb = std::bit_cast<float>(in[3]);
const std::array<std::uint32_t, Results> expected{
a + b, a - b, b - a, a >= b ? 0u : a + 1u, (a == 0u || a > b) ? b : a - 1u,
static_cast<std::uint32_t>(std::min(sa, sb)), static_cast<std::uint32_t>(std::max(sa, sb)), std::min(a, b), std::max(a, b),
a & b, a | b, a ^ b, b,
std::bit_cast<std::uint32_t>(std::min(fa, fb)), std::bit_cast<std::uint32_t>(std::max(fa, fb)), std::bit_cast<std::uint32_t>(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;
}
}

View File

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

View File

@ -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<const std::
} else if (instruction.op == RdnaOpcode::DsWrite2st64B64 || instruction.op == RdnaOpcode::DsRead2st64B64 || instruction.op == RdnaOpcode::DsWrxchg2st64RtnB64) {
instruction.memoryOffset = offset0 * 512u;
instruction.secondaryOffset = offset1 * 512u;
} else if (isDsSrc2Opcode(instruction.op)) {
if ((offset1 & 0x80u) != 0u) {
throw std::runtime_error("DS src2 operation with the offset taken from the address is not supported");
}
instruction.memoryOffset = 0u;
instruction.secondaryOffset = signExtend(combinedOffset & 0x7fffu, 15u) * 4u;
}
instruction.destination = d16Half(vectorRegister(vdst), instruction.op);

View File

@ -116,6 +116,7 @@ private:
bool dsRead2(const RdnaInstruction& inst);
bool dsWrite(const RdnaInstruction& inst);
bool dsWrite2(const RdnaInstruction& inst);
bool dsSrc2(const RdnaInstruction& inst, IrOpcode opcode);
bool dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue);
bool dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue);
bool dsCondxchg32(const RdnaInstruction& inst);

View File

@ -339,6 +339,38 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) {
return dsAtomic(inst, IrOpcode::SharedAtomicXor32, false);
case RdnaOpcode::DsXorRtnB32:
return dsAtomic(inst, IrOpcode::SharedAtomicXor32, true);
case RdnaOpcode::DsAddSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicIAdd32);
case RdnaOpcode::DsSubSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicISub32);
case RdnaOpcode::DsRsubSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicRsub32);
case RdnaOpcode::DsIncSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicInc32);
case RdnaOpcode::DsDecSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicDec32);
case RdnaOpcode::DsMinSrc2I32:
return dsSrc2(inst, IrOpcode::SharedAtomicSMin32);
case RdnaOpcode::DsMaxSrc2I32:
return dsSrc2(inst, IrOpcode::SharedAtomicSMax32);
case RdnaOpcode::DsMinSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicUMin32);
case RdnaOpcode::DsMaxSrc2U32:
return dsSrc2(inst, IrOpcode::SharedAtomicUMax32);
case RdnaOpcode::DsAndSrc2B32:
return dsSrc2(inst, IrOpcode::SharedAtomicAnd32);
case RdnaOpcode::DsOrSrc2B32:
return dsSrc2(inst, IrOpcode::SharedAtomicOr32);
case RdnaOpcode::DsXorSrc2B32:
return dsSrc2(inst, IrOpcode::SharedAtomicXor32);
case RdnaOpcode::DsWriteSrc2B32:
return dsSrc2(inst, IrOpcode::WriteSharedU32);
case RdnaOpcode::DsMinSrc2F32:
return dsSrc2(inst, IrOpcode::SharedAtomicFMin32);
case RdnaOpcode::DsMaxSrc2F32:
return dsSrc2(inst, IrOpcode::SharedAtomicFMax32);
case RdnaOpcode::DsAddSrc2F32:
return dsSrc2(inst, IrOpcode::SharedAtomicFAdd32);
case RdnaOpcode::DsWrxchgRtnB32:
return dsAtomic(inst, IrOpcode::SharedAtomicSwap32, true);

View File

@ -194,6 +194,23 @@ bool TranslationContext::dsWrite2(const RdnaInstruction& inst) {
return true;
}
bool TranslationContext::dsSrc2(const RdnaInstruction& inst, IrOpcode opcode) {
if (inst.gds) {
throw std::runtime_error("GDS src2 operations are not supported");
}
const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst);
const IrU32 address = readU32(inst.source0);
MemoryInfo source = memory;
source.offset = memory.secondaryOffset;
source.secondaryOffset = 0u;
IrValue* value = loadSharedU32(1u, address, source, inst.programCounter);
MemoryInfo target = memory;
target.secondaryOffset = 0u;
IrValue& active = ir.GetExec();
(void)ir.Emit(opcode, IrOpcodeType(opcode), {&address.Value(), value, &active}, addMemoryInfo(target, inst.programCounter));
return true;
}
bool TranslationContext::dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) {
const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst);
const IrU32 address = readU32(inst.source0);

View File

@ -328,6 +328,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode.
- [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits).
- [Raw `buffer_load_dwordx2`](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`.
- [DS src2 operations](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented.
- [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached
- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) passes `%ls` and `%S` to glibc on Linux, which reads them as strings of 32-bit `wchar_t`; the guest's `wchar_t` is 16 bits (UTF-16), so these conversions fail with -1. The Windows path converts them to UTF-8, and `guest_formatting` checks wide conversions on Windows only
- [collate<char>::_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.