feat(shader): implement global_store_dword_addtid

Decode FLAT opcode 0x17 in the global segment and store the data VGPR of
each active lane at the SGPR pair base plus the signed offset plus four
times the lane id, sharing the address path of global_load_dword_addtid.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-05 00:09:14 +02:00
parent 3655c2ae34
commit d044384a89
6 changed files with 198 additions and 7 deletions

View File

@ -0,0 +1,179 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::size_t BlockBytes = 65536;
constexpr std::uint32_t Base = 2048;
constexpr std::uint32_t Fill = 0x5a5a5a5au;
alignas(256) constexpr std::array<std::uint32_t, 41> AddtidCode{
0x800aff08, 0x00000800, 0x820b8009, 0x160800ff, 0x00000101, 0x4a0808ff, 0x00005100, 0x36500083,
0x3a520880, 0xbefc0380, 0xdc5c8000, 0x000a2900, 0x3a5208ff, 0x11110000, 0xbefc03c0, 0xdc5c8f00,
0x000a2900, 0x3a5208ff, 0x22220000, 0xbefc03ff, 0x0000ffff, 0xdc5c8c00, 0x000a2900, 0x3a5208ff,
0x33330000, 0xbefc0380, 0x7daa5082, 0xdc5c8200, 0x000a2900, 0xbefe04c1, 0x3a5208ff, 0x44440000,
0xbefc0380, 0xdc5c8800, 0x000a2900, 0x3a5208ff, 0x55550000, 0xbefc0380, 0xdc5c87fc, 0x000a2900,
0xbf810000,
};
struct Store {
std::int32_t offset;
std::uint32_t pattern;
bool masked;
};
constexpr std::array<Store, 6> Stores{{
{0, 0x00000000u, false},
{-256, 0x11110000u, false},
{-1024, 0x22220000u, false},
{512, 0x33330000u, true},
{-2048, 0x44440000u, false},
{2044, 0x55550000u, false},
}};
class GuestBlock {
public:
GuestBlock() {
#ifdef _WIN32
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
#else
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
#endif
Require(block != nullptr, "global store addtid: cannot allocate the guest block");
GuestAllocations::Mutation().Add(block, BlockBytes, true, true);
}
~GuestBlock() {
GuestAllocations::Mutation().Remove(block);
#ifdef _WIN32
VirtualFree(block, 0, MEM_RELEASE);
#else
std::free(block);
#endif
}
GuestBlock(const GuestBlock&) = delete;
GuestBlock& operator=(const GuestBlock&) = delete;
std::uint8_t* Data() { return block; }
private:
std::uint8_t* block = nullptr;
};
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
std::vector<std::uint32_t> Expected(std::uint32_t lanes) {
std::vector<std::uint32_t> memory(BlockBytes / 4u, Fill);
for (const auto& store : Stores) {
for (std::uint32_t lane = 0; lane < lanes; ++lane) {
if (store.masked && (lane & 3u) == 2u) continue;
memory[(Base + store.offset + lane * 4u) / 4u] = (0x5100u + lane * 0x101u) ^ store.pattern;
}
}
return memory;
}
void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::string& name) {
std::vector<std::uint32_t> memory(BlockBytes / 4u, Fill);
std::memcpy(guest.Data(), memory.data(), BlockBytes);
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(guest.Data()));
std::vector<std::uint32_t> userData(10, 0u);
userData[8] = static_cast<std::uint32_t>(address);
userData[9] = static_cast<std::uint32_t>(address >> 32u);
const std::span<const std::uint32_t> code(AddtidCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> regions{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, 0, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions},
target,
{0, 0, 0, 128}
};
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
std::memcpy(memory.data(), guest.Data(), BlockBytes);
const auto expected = Expected(waveSize);
for (std::size_t dword = 0; dword < memory.size(); ++dword) {
Require(memory[dword] == expected[dword], "global store addtid: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword]));
}
}
void CheckRejected(const AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::string& reason) {
const std::array<ShaderRecompiler::MemoryRegion, 1> regions{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0, {false, false, false}, false, 1};
const std::vector<std::uint32_t> userData(10, 0u);
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32, 0, userData, compute, std::nullopt, std::nullopt, regions},
device.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
std::string failure;
try {
static_cast<void>(ShaderRecompiler::Recompile(request));
} catch (const std::exception& error) {
failure = error.what();
}
Require(failure.find(reason) != std::string::npos, "global store addtid: expected '" + reason + "', got '" + failure + "'");
}
void CheckRejections(const AgcDriver::VulkanDevice& device) {
alignas(256) static constexpr std::array<std::uint32_t, 3> flatSegment{0xdc5c0000, 0x000a2900, 0xbf810000};
alignas(256) static constexpr std::array<std::uint32_t, 3> scratchSegment{0xdc5c4000, 0x000a2900, 0xbf810000};
alignas(256) static constexpr std::array<std::uint32_t, 3> noScalarBase{0xdc5c8000, 0x007d2900, 0xbf810000};
CheckRejected(device, flatSegment, "global_store_dword_addtid is available only in the global segment");
CheckRejected(device, scratchSegment, "global_store_dword_addtid is available only in the global segment");
CheckRejected(device, noScalarBase, "global_store_dword_addtid supports only an SGPR pair as base address");
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
CheckRejections(*device);
if (device->Target().subgroupSize < 32u) {
std::printf("skipped, the device's subgroups are narrower than a wave (%u lanes)\n", device->Target().subgroupSize);
return VulkanTestSkipped;
}
GuestBlock guest;
Run(*device, guest, 32, device->Target(), "wave32");
Run(*device, guest, 64, device->Target(), "wave64");
Run(*device, guest, 64, device->ComputeTarget(32), "wave64 split");
std::puts("global store addtid tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -832,6 +832,7 @@ enum class RdnaOpcode : std::uint16_t {
FlatAtomicFmaxX2,
FlatAtomicIncX2,
FlatAtomicDecX2,
GlobalStoreDwordAddtid,
DsAddU32,
DsAddRtnU32,
DsSubU32,

View File

@ -3,6 +3,7 @@
#include <bit>
#include <limits>
#include <stdexcept>
#include <string>
namespace ShaderRecompiler {
@ -170,6 +171,7 @@ constexpr MemoryOpcodeInfo flatOpcodes[] = {
{0x0eu, RdnaOpcode::FlatLoadDwordx4, 4, 32, false, false, false},
{0x0fu, RdnaOpcode::FlatLoadDwordx3, 3, 32, false, false, false},
{0x16u, RdnaOpcode::GlobalLoadDwordAddtid, 1, 32, false, false, false},
{0x17u, RdnaOpcode::GlobalStoreDwordAddtid, 1, 32, false, false, false},
{0x18u, RdnaOpcode::FlatStoreByte, 1, 8, false, false, false},
{0x1au, RdnaOpcode::FlatStoreShort, 1, 16, false, false, false},
{0x1cu, RdnaOpcode::FlatStoreDword, 1, 32, false, false, false},
@ -560,7 +562,8 @@ bool isFlatStoreOpcode(RdnaOpcode opcode) {
case RdnaOpcode::FlatStoreDword:
case RdnaOpcode::FlatStoreDwordx2:
case RdnaOpcode::FlatStoreDwordx3:
case RdnaOpcode::FlatStoreDwordx4: return true;
case RdnaOpcode::FlatStoreDwordx4:
case RdnaOpcode::GlobalStoreDwordAddtid: return true;
default: return false;
}
}
@ -831,11 +834,12 @@ RdnaInstruction DecodeRdnaFlat(std::uint32_t programCounter, std::span<const std
setRawWords(instruction, code, wordIndex, 2u);
instruction.destination = d16Half(vectorRegister(isFlatStoreOpcode(instruction.op) ? data : vdst), instruction.op);
if (instruction.op == RdnaOpcode::GlobalLoadDwordAddtid) {
if (instruction.op == RdnaOpcode::GlobalLoadDwordAddtid || instruction.op == RdnaOpcode::GlobalStoreDwordAddtid) {
const std::string name = instruction.op == RdnaOpcode::GlobalLoadDwordAddtid ? "global_load_dword_addtid" : "global_store_dword_addtid";
if (seg != 2u) {
throw std::runtime_error("global_load_dword_addtid is available only in the global segment");
throw std::runtime_error(name + " is available only in the global segment");
}
instruction.source0 = scalarDescriptorBase(saddr, 2u, "global_load_dword_addtid supports only an SGPR pair as base address");
instruction.source0 = scalarDescriptorBase(saddr, 2u, (name + " supports only an SGPR pair as base address").c_str());
instruction.sourceCount = 1;
return instruction;
}

View File

@ -114,7 +114,7 @@ private:
bool dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue);
bool dsAppendConsume(const RdnaInstruction& inst, IrOpcode opcode);
bool dsAddtid(const RdnaInstruction& inst, bool write);
bool globalLoadAddtid(const RdnaInstruction& inst);
bool globalAddtid(const RdnaInstruction& inst, bool write);
bool dsSwizzleB32(const RdnaInstruction& inst);
bool dsBpermuteB32(const RdnaInstruction& inst);
bool dsPermuteB32(const RdnaInstruction& inst);

View File

@ -97,7 +97,7 @@ bool TranslationContext::flatLoad(const RdnaInstruction& inst) {
return true;
}
bool TranslationContext::globalLoadAddtid(const RdnaInstruction& inst) {
bool TranslationContext::globalAddtid(const RdnaInstruction& inst, bool write) {
MemoryInfo memory = flatMemoryInfoFromInstruction(inst);
memory.addressIsFull = false;
const IrU32 baseLow = readU32(inst.source0);
@ -106,6 +106,11 @@ bool TranslationContext::globalLoadAddtid(const RdnaInstruction& inst) {
IrValue& lane = ir.Emit(IrOpcode::LaneId, IrOpcodeType(IrOpcode::LaneId), {});
IrValue& laneOffset = ir.ShiftLeftLogical(lane, ir.Constant(2u));
IrValue& active = ir.GetExec();
if (write) {
const IrU32 data = readU32(inst.destination);
(void)ir.Emit(IrOpcode::StoreAddressU32, IrType::Void, {resource, &laneOffset, &ir.Constant(0u), &data.Value(), &active}, addMemoryInfo(memory, inst.programCounter));
return true;
}
IrValue& loaded = ir.Emit(IrOpcode::LoadAddressU32, IrOpcodeType(IrOpcode::LoadAddressU32), {resource, &laneOffset, &ir.Constant(0u), &active}, addMemoryInfo(memory, inst.programCounter));
writeOperand(inst.destination, &loaded);
return true;

View File

@ -197,7 +197,9 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) {
case RdnaOpcode::FlatLoadDwordx4:
return flatLoad(inst);
case RdnaOpcode::GlobalLoadDwordAddtid:
return globalLoadAddtid(inst);
return globalAddtid(inst, false);
case RdnaOpcode::GlobalStoreDwordAddtid:
return globalAddtid(inst, true);
case RdnaOpcode::FlatStoreByte:
case RdnaOpcode::FlatStoreShort:
case RdnaOpcode::FlatStoreByteD16Hi: