From 50e9cc40218e324eb3ee371ac1234c83093f90b5 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 18:50:15 +0300 Subject: [PATCH 1/2] perf(recompiler): translate GCN buffer formats without a private table Registration modules read a GCN V#'s word 3 in the shader, and gcnWord3 mapped its DATA_FORMAT and NUM_FORMAT through a 128-entry Private array that every buffer access stored again before indexing it. Drivers keep that array in per-invocation scratch: on the RX 9070 XT each pixel shader of Monopoly Plus's language screen used 528 bytes of scratch and wrote the table up to 60 times per invocation. The lookup is now a helper function that packs the table four entries to a word and picks the word with a select tree, so it stays in registers and each access calls it once. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../tests/execution/GcnBufferFormatLookup.cpp | 211 ++++++++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 2 +- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 54 ++++- 4 files changed, 257 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2bb2a650aa..746e8f0366 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2200,6 +2200,7 @@ target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) target_include_directories(agc_driver_failed_capture_dump_tests PRIVATE $) +target_include_directories(agc_driver_gcn_buffer_format_lookup_tests PRIVATE $) target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp new file mode 100644 index 0000000000..1254599280 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp @@ -0,0 +1,211 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t Vertices = 3; +constexpr std::uint32_t Fill = 0xdeadbeefu; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uintptr_t GcnAddressLimit = std::uintptr_t{1} << 44u; +constexpr std::array InputWords{0x12345678u, 0x9abcdef0u, 0x0badf00du, 0x600dcafeu}; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0300000u, 0x80010100u, 0xbf8c0f70u, 0xe0700000u, 0x80020100u, 0xf80008cfu, 0x03020100u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, +}; + +class LowBlock { +public: + LowBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + void* mapped = mmap(reinterpret_cast(std::uintptr_t{1} << 36u), BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + block = mapped == MAP_FAILED ? nullptr : static_cast(mapped); +#endif + Require(block != nullptr, "GCN buffer format lookup: cannot allocate the buffer block"); + Require(reinterpret_cast(block) + BlockBytes <= GcnAddressLimit, "GCN buffer format lookup: the buffer block lies above the 44-bit GCN V# address range"); + } + + ~LowBlock() { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, BlockBytes); +#endif + } + + LowBlock(const LowBlock&) = delete; + LowBlock& operator=(const LowBlock&) = delete; + + std::span Input() const { return std::span(block, 4); } + std::span Output() const { return std::span(block + 64, 4); } + +private: + std::uint32_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes, std::uint32_t dataFormat, std::uint32_t numberFormat) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0xfacu | (numberFormat << 12u) | (dataFormat << 15u)}; +} + +std::vector UserData(const LowBlock& block, std::uint32_t dataFormat, std::uint32_t numberFormat) { + std::vector userData(12, 0u); + const auto input = BufferDescriptor(block.Input().data(), 16u, dataFormat, numberFormat); + const auto output = BufferDescriptor(block.Output().data(), 16u, 4u, 4u); + std::copy(input.begin(), input.end(), userData.begin() + 4); + std::copy(output.begin(), output.end(), userData.begin() + 8); + return userData; +} + +ShaderRecompiler::RecompileRequest VertexRequest(std::span userData, const ShaderRecompiler::SpirvTarget& target, std::span memory) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, memory}, + target, + {0, 0, 0, 64} + }; + request.useCache = false; + return request; +} + +std::uint32_t PrivateStores(std::span words) { + std::set privateMemory; + std::uint32_t stores = 0; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Require(count != 0 && count <= words.size() - offset, "GCN buffer format lookup: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpVariable && count >= 4 && words[offset + 3] == spv::StorageClassPrivate) privateMemory.insert(words[offset + 2]); + if ((opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain) && count >= 4 && privateMemory.contains(words[offset + 3])) privateMemory.insert(words[offset + 2]); + if (opcode == spv::OpStore && count >= 3 && privateMemory.contains(words[offset + 1])) ++stores; + offset += count; + } + return stores; +} + +void CheckModule(const AgcDriver::VulkanDevice& device, const LowBlock& block, std::span memory) { + auto target = device.Target(); + target.registrationModules = true; + const auto userData = UserData(block, 4u, 4u); + const auto result = ShaderRecompiler::Recompile(VertexRequest(userData, target, memory)); + Require(result.registrationModule, "GCN buffer format lookup: the vertex shader was not emitted in registration form"); + const auto stores = PrivateStores(result.spirv.Words()); + Require(stores == 0u, "GCN buffer format lookup: the registration module stores " + std::to_string(stores) + " times into private memory to translate DATA_FORMAT and NUM_FORMAT, which drivers keep in per-invocation scratch"); +} + +void Draw(AgcDriver::VulkanDevice& device, const LowBlock& block, std::uint32_t dataFormat, std::uint32_t numberFormat, std::span vertexMemory, const ShaderRecompiler::RecompileResult& pixelResult, std::uint32_t vertexPush, std::vector& registrationModule) { + Pixels.fill(std::byte{0x40}); + std::copy(InputWords.begin(), InputWords.end(), block.Input().begin()); + std::fill(block.Output().begin(), block.Output().end(), Fill); + const auto userData = UserData(block, dataFormat, numberFormat); + const auto vertexResult = ShaderRecompiler::Recompile(VertexRequest(userData, device.Target(), vertexMemory)); + const auto what = "DATA_FORMAT " + std::to_string(dataFormat) + " NUM_FORMAT " + std::to_string(numberFormat); + Require(static_cast(vertexResult.pushConstants.size()) == vertexPush, "GCN buffer format lookup, " + what + ": the vertex push constants changed size"); + if (device.Target().registrationModules) { + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Copy(); + Require(std::ranges::equal(vertexResult.spirv.Words(), registrationModule), "GCN buffer format lookup, " + what + ": registration modules: the V# specialized the vertex module"); + } + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, Vertices, 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + const auto translated = ShaderRecompiler::GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; + const auto expected = translated != 0u ? InputWords[0] : 0u; + const auto output = block.Output(); + char text[96]; + std::snprintf(text, sizeof(text), ": translated format %u, loaded 0x%08x, expected 0x%08x", translated, output[0], expected); + Require(output[0] == expected, "GCN buffer format lookup, " + what + text); + Require(std::all_of(output.begin() + 1, output.end(), [](std::uint32_t value) { return value == Fill; }), "GCN buffer format lookup, " + what + ": the store wrote past its dword"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + ShaderRecompiler::SetGcnGuestShaders(true); + const LowBlock block; + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + CheckModule(*device, block, vertexMemory); + const auto firstUserData = UserData(block, 4u, 4u); + const auto vertexPush = static_cast(ShaderRecompiler::Recompile(VertexRequest(firstUserData, device->Target(), vertexMemory)).pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + device->Target(), + PixelPushLayout(vertexPush, device->Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector registrationModule; + for (std::uint32_t dataFormat = 0; dataFormat < 16u; ++dataFormat) { + for (std::uint32_t numberFormat = 0; numberFormat < 8u; ++numberFormat) Draw(*device, block, dataFormat, numberFormat, vertexMemory, pixelResult, vertexPush, registrationModule); + } + std::puts("GCN buffer format lookup tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 92ea828d77..57020370da 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -187,7 +187,7 @@ struct SpirvEmitterState { std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; std::uint32_t srgbTableVariable = 0; - std::uint32_t gcnBufferFormatVariable = 0; + std::uint32_t gcnBufferFormatFunction = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index b71a38a1c1..130481696d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -50,18 +50,52 @@ std::uint32_t nonzero(SpirvEmitterState& state, std::uint32_t value) { return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0u)); } +std::uint32_t gcnBufferFormat(SpirvEmitterState& state, std::uint32_t index) { + const auto u32 = TypeU32(state); + if (state.gcnBufferFormatFunction == 0u) { + const auto id = state.module.AllocateId(); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel}; + state.module.AddName(id, "gcn_buffer_format"); + state.module.AddFunction(spv::OpFunction, u32, id, spv::FunctionControlMaskNone, state.module.Type(spv::OpTypeFunction, u32, u32)); + const auto parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, parameter); + EmitLabel(state, state.module.AllocateId()); + const auto& formats = GcnBufferFormats(); + std::vector words; + for (std::size_t entry = 0; entry < formats.size(); entry += 4u) { + std::uint32_t packed = 0; + for (std::uint32_t byte = 0; byte < 4u; ++byte) packed |= static_cast(formats[entry + byte]) << (byte * 8u); + words.push_back(ConstantU32(state, packed)); + } + const auto word = Binary(state, spv::OpShiftRightLogical, u32, parameter, ConstantU32(state, 2u)); + for (std::uint32_t bit = 0; words.size() > 1u; ++bit) { + const auto odd = nonzero(state, Binary(state, spv::OpBitwiseAnd, u32, word, ConstantU32(state, 1u << bit))); + std::vector halves; + for (std::size_t pair = 0; pair < words.size(); pair += 2u) halves.push_back(words[pair] == words[pair + 1u] ? words[pair] : Select(state, u32, odd, words[pair + 1u], words[pair])); + words = std::move(halves); + } + const auto shift = Binary(state, spv::OpShiftLeftLogical, u32, Binary(state, spv::OpBitwiseAnd, u32, parameter, ConstantU32(state, 3u)), ConstantU32(state, 3u)); + state.module.AddFunction(spv::OpReturnValue, EmitBitFieldUExtract(state, words.front(), shift, ConstantU32(state, 8u))); + state.module.AddFunction(spv::OpFunctionEnd); + state.gcnBufferFormatFunction = id; + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u32, result, state.gcnBufferFormatFunction, index); + return result; +} + std::uint32_t gcnWord3(SpirvEmitterState& state, std::uint32_t word1, std::uint32_t word3) { const auto u32 = TypeU32(state); - const auto& formats = GcnBufferFormats(); - std::array words{}; - for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, formats[index]); - const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, static_cast(words.size()))); - if (state.gcnBufferFormatVariable == 0) state.gcnBufferFormatVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); - state.module.AddFunction(spv::OpStore, state.gcnBufferFormatVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, state.gcnBufferFormatVariable, field(state, word3, 12u, 7u)); - const auto format = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, u32, format, pointer); + const auto format = gcnBufferFormat(state, field(state, word3, 12u, 7u)); const auto outOfBounds = Select(state, u32, nonzero(state, field(state, word1, 16u, 14u)), ConstantU32(state, 1u << 28u), ConstantU32(state, 3u << 28u)); const auto kept = Binary(state, spv::OpBitwiseAnd, u32, word3, ConstantU32(state, 0xc0e00fffu)); return Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, kept, Binary(state, spv::OpShiftLeftLogical, u32, format, ConstantU32(state, 12u))), outOfBounds); From 692d0db8f79b3a66ca43efd888ebc1200865281c Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Sun, 25 Oct 2026 23:46:11 +0200 Subject: [PATCH 2/2] test(agc): place the GCN buffer format lookup block in the guest arena On Linux libc reserves the whole guest arena at start-up, so the mmap hint at 0x1000000000 was taken and the kernel put the buffer block near the top of the address space, above the 44 bits a GCN V# carries. The hosted Linux runner failed the test there. The block now comes from the guest arena on every platform, which ends at 0xFC00000000. If it still lies above 2^44, as in an ASan build that moves the arena up, the test prints the reason and skips. The stored dwords are read back through GuestMemory::Read. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/GcnBufferFormatLookup.cpp | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp index 1254599280..e85dcfb736 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp @@ -1,17 +1,11 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "Recompiler.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "VulkanTestDevice.hpp" -#ifdef _WIN32 -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#else -#include -#endif #include #include #include @@ -48,31 +42,30 @@ alignas(256) constexpr std::array PixelCode{ class LowBlock { public: - LowBlock() { -#ifdef _WIN32 - block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); -#else - void* mapped = mmap(reinterpret_cast(std::uintptr_t{1} << 36u), BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - block = mapped == MAP_FAILED ? nullptr : static_cast(mapped); -#endif - Require(block != nullptr, "GCN buffer format lookup: cannot allocate the buffer block"); - Require(reinterpret_cast(block) + BlockBytes <= GcnAddressLimit, "GCN buffer format lookup: the buffer block lies above the 44-bit GCN V# address range"); + LowBlock() : block(static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, BlockBytes))) { + Require(block != nullptr, "GCN buffer format lookup: cannot allocate the buffer block in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, 0x04u, BlockBytes); } ~LowBlock() { -#ifdef _WIN32 - VirtualFree(block, 0, MEM_RELEASE); -#else - munmap(block, BlockBytes); -#endif + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); } LowBlock(const LowBlock&) = delete; LowBlock& operator=(const LowBlock&) = delete; + std::uintptr_t Address() const { return reinterpret_cast(block); } + bool Addressable() const { return Address() + BlockBytes <= GcnAddressLimit; } std::span Input() const { return std::span(block, 4); } std::span Output() const { return std::span(block + 64, 4); } + std::array Stored() const { + std::array words{}; + AgcDriver::GuestMemory::Read(reinterpret_cast(Output().data()), std::as_writable_bytes(std::span(words)), 4); + return words; + } + private: std::uint32_t* block = nullptr; }; @@ -163,7 +156,7 @@ void Draw(AgcDriver::VulkanDevice& device, const LowBlock& block, std::uint32_t device.WaitIdle(); const auto translated = ShaderRecompiler::GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; const auto expected = translated != 0u ? InputWords[0] : 0u; - const auto output = block.Output(); + const auto output = block.Stored(); char text[96]; std::snprintf(text, sizeof(text), ": translated format %u, loaded 0x%08x, expected 0x%08x", translated, output[0], expected); Require(output[0] == expected, "GCN buffer format lookup, " + what + text); @@ -178,6 +171,10 @@ int main() { if (!device) return VulkanTestSkipped; ShaderRecompiler::SetGcnGuestShaders(true); const LowBlock block; + if (!block.Addressable()) { + std::printf("skipped, the guest arena block at 0x%llx lies above the 44-bit GCN V# address range\n", static_cast(block.Address())); + return VulkanTestSkipped; + } const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; CheckModule(*device, block, vertexMemory); const auto firstUserData = UserData(block, 4u, 4u);