From ddb66806678a635b59cad76954dc277d0bd65457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 10:18:14 +0200 Subject: [PATCH 01/19] perf(recompiler): read buffer V# words in the shader when modules are built at registration Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/src/VulkanDevice.cpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 14 ++- .../execution/BufferDescriptorRuntime.cpp | 113 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../Optimization/DescriptorBindingBuilder.hpp | 2 +- .../src/DescriptorBindingBuilder.cpp | 14 ++- core/shader/recompiler/Recompiler.cpp | 7 +- core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 42 ++++--- 14 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6c94ffc4b..d7f9bef59 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1010,6 +1010,8 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) + target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d24dbbc56..858e855dc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2926,6 +2926,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.fixedPushSlots = state->graphicsPipelineLibrary; + target.registrationModules = state->graphicsPipelineLibrary; target.srgbDecodeFormats = state->srgbDecodeFormats; if (state->meshShader) { const auto& mesh = state->meshLimits; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1526845de..e191f6d1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1625,12 +1625,12 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); + require(requestPrefix(encoded, 8u) == "NVNQQREAAAA=", "new requests did not use serialization version 17"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; for (std::size_t bytes = 0; bytes < 18u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source or alpha-to-coverage flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRIAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1725,7 +1725,7 @@ void verifyLegacyPixelRequests() { require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); - require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve, "legacy request trailer was misread"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread"); const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); } @@ -3218,6 +3218,14 @@ int main(int argc, char** argv) { auto float64Denormals = uncached; float64Denormals.target.float64DenormPreserve = true; require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization"); + auto registrationModules = uncached; + registrationModules.target.registrationModules = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target.registrationModules, "registration modules were lost in serialization"); + std::vector plainKey; + std::vector registrationKey; + RecompileCacheKey::Build(uncached, plainKey); + RecompileCacheKey::Build(registrationModules, registrationKey); + require(plainKey != registrationKey, "registration modules did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp new file mode 100644 index 000000000..1948f7e20 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -0,0 +1,113 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t InputBytes = 256; +constexpr std::uint32_t Fill = 0xdeadbeefu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0xe0701000, 0x80010201, 0xbf810000, +}; + +struct Case { + const char* name; + std::uint32_t baseOffset; + std::uint32_t records; + bool present; +}; + +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, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +ShaderRecompiler::RecompileRequest Request(std::span userData, const ShaderRecompiler::SpirvTarget& target, std::span memory) { + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + return { + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; +} + +void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { + const std::string mode = registrationModules ? "registration modules" : "specialized modules"; + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + std::array userData{}; + auto target = device.ComputeTarget(Lanes); + target.registrationModules = registrationModules; + const auto request = Request(userData, target, memory); + const auto handle = ShaderRecompiler::PrepareShader(request); + const std::array cases{{ + {"aligned", 0, 128, true}, + {"base +1", 1, 128, true}, + {"base +2", 2, 128, true}, + {"base +3", 3, 128, true}, + {"records 37", 0, 37, true}, + {"records 64 base +2", 2, 64, true}, + {"absent", 0, 128, false}, + }}; + const void* first = nullptr; + for (const auto& test : cases) { + Output.fill(Fill); + const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + ShaderRecompiler::SrtRuntime runtime{}; + runtime.userData = userData; + const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); + const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); + if (first == nullptr) first = result->spirv.data(); + if (registrationModules) Require(result->spirv.data() == first, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + std::uint32_t expected = 0; + if (test.present && lane * 4u + 4u <= test.records) std::memcpy(&expected, Input.data() + test.baseOffset + lane * 4u, sizeof(expected)); + Require(Output[lane] == expected, "buffer descriptor runtime, " + mode + ", " + test.name + ": lane " + std::to_string(lane) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast(index * 41u + 3u + (index >> 3u)); + Run(*device, false); + Run(*device, true); + std::puts("buffer descriptor runtime tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index be68766aa..b50278685 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -240,6 +240,7 @@ private: append(key, value.srgbDecodeFormats); append(key, value.narrowSubgroupClock); append(key, value.float64DenormPreserve); + append(key, value.registrationModules); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 5a76e67bb..b2f5d2007 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -734,7 +734,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(16u); + writer.WriteU32(17u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -754,6 +754,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const if (request.context.compute.has_value()) writer.WriteU32(request.context.compute->scratchDwords); writer.WriteBool(request.target.narrowSubgroupClock); writer.WriteBool(request.target.float64DenormPreserve); + writer.WriteBool(request.target.registrationModules); return base64Encode(buffer); } @@ -762,7 +763,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 17u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -783,6 +784,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 10u && result.request.context.compute.has_value()) result.request.context.compute->scratchDwords = reader.ReadU32(); if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool(); if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); return result; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 01ece57e4..224a452d9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -33,7 +33,7 @@ struct DescriptorBindingPlan { class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}) const; + DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6b68b292a..32505b852 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -250,7 +250,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con allocation.specialization = std::move(plan.specialization); } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; @@ -280,11 +280,13 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); - const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; - plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); - plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); - plan.specialization.push_back({first + 2u, present ? 1u : 0u}); - plan.specialization.push_back({first + 3u, misalignment}); + if (!registrationModules) { + const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; + plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); + plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); + plan.specialization.push_back({first + 2u, present ? 1u : 0u}); + plan.specialization.push_back({first + 3u, misalignment}); + } if (buffer.formattedReadMask != 0u) { for (std::uint32_t component = 0; component < 4u; ++component) { if ((buffer.formattedReadMask & (1u << component)) == 0u) continue; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 4b9660270..5fda2ba0b 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -418,6 +418,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; + targetOptions.registrationModules = request.target.registrationModules; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; @@ -601,7 +602,7 @@ struct PreparedBindingPlan { std::map, std::shared_ptr> modules; }; -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports) { +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool registrationModules) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -642,7 +643,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, registrationModules); } catch (...) { plan->failure = std::current_exception(); } @@ -704,7 +705,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .barycentricEmulation = artifact.barycentricEmulation, .variantId = artifact.variantId }; - const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}); + const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; struct LocalModuleKeyStorage {}; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 85f9689e8..731ffa1c7 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -229,6 +229,7 @@ struct SpirvTarget { bool narrowSubgroupClock = false; bool fixedPushSlots = false; bool float64DenormPreserve = false; + bool registrationModules = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 0d18fb6e5..dbd24615a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -21,6 +21,7 @@ struct SpirvTargetOptions { bool nonConstantImageOffsets = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool registrationModules = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index ca69aa93c..ff712c17e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -91,6 +91,8 @@ struct SpirvEmitterState { bool splitSubgroup = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool registrationModules = false; + std::uint32_t bufferMisalignment = 0; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8a0951153..df64b366a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -250,6 +250,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.nonConstantImageOffsets = target.nonConstantImageOffsets; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; + state.registrationModules = target.registrationModules; state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 2e6a1b0db..0a3a991da 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -57,9 +57,10 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto* handle = instruction.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) context.Fail(instruction, "requires a runtime four-dword V#"); const auto& memory = context.Memory(instruction); + const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = memory.gpuDescriptor ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); - const auto word3 = memory.gpuDescriptor ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + const auto word3 = runtime ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { @@ -82,7 +83,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst } if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); - const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present; return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } @@ -140,8 +142,10 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& instruction) { const auto& memory = context.Memory(instruction); if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u); - const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - return context.state.module.SpecializationConstant(TypeU32(context.state), first + 3u, 0u); + auto& state = context.state; + if (state.bufferMisalignment != 0u) return state.bufferMisalignment; + if (state.registrationModules) return Binary(state, spv::OpBitwiseAnd, TypeU32(state), context.Arg(*instruction.Argument(0)->Resolve(), 0u), ConstantU32(state, 3u)); + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u); } std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t address) { @@ -401,7 +405,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr auto function = state.formattedBufferFunctions.find(key); if (function == state.formattedBufferFunctions.end()) { const auto id = state.module.AllocateId(); - const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32); const auto label = state.currentLabel; const auto byteOffset = state.memoryByteOffsets.at(element); struct Restore { @@ -412,27 +416,23 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr ~Restore() { state.module.EndHelperFunction(); state.memoryByteOffsets.at(element) = byteOffset; + state.bufferMisalignment = 0u; state.currentLabel = label; } }; state.module.BeginHelperFunction(); const Restore restore{state, element, label, byteOffset}; state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); - std::array parameters{}; - for (std::uint32_t index = 0; index < (store ? 6u : 5u); ++index) { + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 11u : 10u); ++index) { parameters[index] = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionParameter, index == 5u ? valueType : u32, parameters[index]); + state.module.AddFunction(spv::OpFunctionParameter, index == 10u ? valueType : u32, parameters[index]); } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; - const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = state.module.SpecializationConstant(u32, first, 0u); - const auto word3 = state.module.SpecializationConstant(u32, first + 1u, 0u); - const auto stride = field(state, word1, 16u, 14u); - const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); - const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); - const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], stride, word3, swizzle, parameters[0], valid, parameters[1], parameters[2]}; - const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[5] : 0u); + state.bufferMisalignment = parameters[9]; + const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], parameters[5], parameters[6], nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); state.module.AddFunction(spv::OpFunctionEnd); @@ -440,8 +440,12 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr } const auto offset = Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), ConstantU32(state, memory.offset)); const auto result = state.module.AllocateId(); - if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), data); - else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element)); + const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); }; + const auto swizzle = flag(resource.swizzle); + const auto valid = flag(resource.valid); + const auto misalignment = baseMisalignment(context, instruction); + if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment, data); + else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment); return store ? ConstantU32(state, 0u) : result; } From 765ea6a10ffeb21bd6e350aa71b9466e9b580f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 13:35:13 +0200 Subject: [PATCH 02/19] perf(recompiler): keep descriptor heaps uncompacted and read image modes at run time when modules are built at registration Co-Authored-By: Claude --- .../tests/execution/EmulatedColorCompare.cpp | 2 + .../tests/execution/TypedHeaps.cpp | 3 +- core/shader/recompiler/Deserialize.cpp | 2 +- .../include/Optimization/BindingAllocator.hpp | 2 +- .../Optimization/DescriptorBindingBuilder.hpp | 1 + .../Optimization/src/BindingAllocator.cpp | 4 +- .../src/DescriptorBindingBuilder.cpp | 38 ++++++++++++++----- core/shader/recompiler/Recompiler.cpp | 2 +- .../SpirvMemory/SpirvDescriptors.hpp | 1 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 17 ++------- .../src/SpirvMemory/SpirvDescriptors.cpp | 21 +++++++++- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- 12 files changed, 64 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index cc3452070..3347835d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -243,6 +243,7 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { + if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); } @@ -293,6 +294,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char Output.fill(-1.0f); const auto result = Compile(device, Format8888UNorm, sampler, CubeCode, false, true, true); for (const auto& binding : result.bindings) { + if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "cube emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "cube emulated comparison retained an unused sampler association"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp index 77b95c736..ba1c7bb84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp @@ -30,7 +30,7 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: const auto instructions = store ? std::span(storeCode) : std::span(code); ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(instructions.data()), instructions, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u}}; auto result = ShaderRecompiler::Recompile(request); - Require(result.runtimeImageCount == 0u, "direct image allocated runtime image metadata"); + Require(result.runtimeImageCount == (request.target.registrationModules ? 1u : 0u), "direct image runtime metadata does not follow the module mode"); Require(result.shaderDataDwords < ShaderRecompiler::RuntimeAbi::ShaderDataDwords, "shader data was not compacted"); for (const auto& binding : result.bindings) { if (binding.role == ShaderRecompiler::DescriptorRole::GuestImages) Require(binding.count == 1u, "single image expanded into unused descriptor slots"); @@ -48,6 +48,7 @@ void Run(AgcDriver::VulkanDevice& device) { device.Dispatch(shaders.back(), 1u, 1u, 1u); device.SubmitRecorded(); } + if (device.Target().registrationModules) Require(shaders[0].spirv == shaders[1].spirv && shaders[0].spirv == shaders[2].spirv, "registration modules: a present or a null image specialized the module"); shaders.clear(); device.WaitIdle(); for (std::uint32_t batch = 0u; batch < outputs.size(); ++batch) { diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index b4e12b4b5..6f1b3a0ca 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -226,7 +226,7 @@ int main(int argc, char** argv) { std::cout << "ShaderInfoCollector::Collect OK\n"; BindingAllocator bindingAllocator; - const auto bindings = bindingAllocator.Allocate(program, request.layout); + const auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); std::cout << "BindingAllocator::Allocate OK, bindings=" << bindings.bindings.size() << "\n"; std::cout << "ALL STAGES OK\n"; diff --git a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp index 120de5358..0944d2353 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp @@ -23,7 +23,7 @@ struct BindingAllocationResult : CompiledBindingLayout { class BindingAllocator { public: - [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout) const; + [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false) const; [[nodiscard]] const IrDescriptorBinding& FindBinding(const IrBindingLayout& layout, DescriptorBindingKind kind) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 224a452d9..d600e6fbb 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -14,6 +14,7 @@ struct PreparedDescriptorBinding { std::vector resources; std::vector identityImageSwizzle; std::vector samplerFilterElements; + std::vector inactive; std::size_t writtenBuffers = 0; std::size_t readOnlyBuffers = 0; }; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index f99616a96..822cc38b7 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -68,7 +68,7 @@ bool usesGds(const IrProgram& program) { } -BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout) const { +BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules) const { IrProgramMetadata& metadata = program.Metadata(); if (!metadata.shaderInfoComplete || metadata.bindingLayoutComplete) { fail(metadata.shaderInfoComplete ? "shader binding layout failed: binding layout already allocated" @@ -102,7 +102,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin next.dispatchThreadLimit = info.dispatchThreadLimit; if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); - if (std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 32505b852..bb333af47 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -295,7 +295,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } } - for (std::uint32_t index = 0; index < info.images.size(); ++index) { + for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { if (info.images[index].indirectRoot != ImageResource::NoIndirectImage) continue; const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); @@ -327,7 +327,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = (word >> 12u) & 0xfu; const auto last = (word >> 16u) & 0xfu; if (last < first || last - first >= RuntimeAbi::StorageMipSlots) fail("invalid dynamic storage mip range"); - plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); + if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); } for (std::uint32_t resource = 0; resource < info.images.size(); ++resource) { const auto& image = info.images[resource]; @@ -340,7 +340,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (mode != imageModes[slot]) fail("indirect image mode numbering disagrees with its root"); active[mode] = true; } - for (std::uint32_t mode = 0; mode < modes.size(); ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); + for (std::uint32_t mode = 0; mode < modes.size() && !registrationModules; ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); } plan.bindings.reserve(layout.descriptors.size()); for (const auto& logical : layout.descriptors) { @@ -348,6 +348,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const bool samplerHeap = logical.kind == DescriptorBindingKind::Samplers; IrDescriptorBinding compact{logical.kind, {}}; std::vector originals; + std::vector inactive; std::uint32_t previous = UINT32_MAX; std::uint32_t mip = 0; for (std::uint32_t element = 0; element < logical.resources.size(); ++element) { @@ -365,13 +366,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } else if (samplerHeap) { active = (samplerModes.at(resource) & (1u << (element & 1u))) != 0u; } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); - if (active) { + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); + if (active || ((imageHeap || samplerHeap) && registrationModules)) { compact.resources.push_back(resource); originals.push_back(element); + inactive.push_back(!active); } } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; @@ -406,7 +408,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.imageUnnormalizedBaseLevel.push_back(unnormalized.imagesAtBaseLevel.at(resource)); physical.imageSamplers.push_back(0u); physical.imageAtomic64.push_back(image.atomic64); - if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage) continue; + if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage || inactive[element]) continue; if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); const auto& descriptor = snapshot.images.at(resource); RuntimeAbi::ResourceMetadata metadata{}; @@ -428,20 +430,34 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource)); const auto mode = 1u << (originals[element] & 1u); physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u); - if ((originals[element] & 1u) != 0u) entry.samplerFilterElements.push_back(element); + if ((originals[element] & 1u) != 0u && !inactive[element]) entry.samplerFilterElements.push_back(element); } } else if (physical.role == DescriptorRole::ShaderData && layout.UsesPushData()) { fail("DescriptorBindingBuilder::Populate shader-data binding must not exist when push data is used"); } plan.storageData |= physical.role == DescriptorRole::ShaderData; entry.resources = std::move(compact.resources); + if (registrationModules && (imageHeap || samplerHeap)) entry.inactive = std::move(inactive); plan.bindings.push_back(std::move(entry)); } + for (std::uint32_t resource = 0; resource < info.images.size() && registrationModules && layout.runtimeImageCount != 0u; ++resource) { + if (info.images[resource].indirectRoot != ImageResource::NoIndirectImage) continue; + if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); + const auto& descriptor = snapshot.images.at(resource); + RuntimeAbi::ResourceMetadata metadata{}; + if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width"); + metadata.elementCount = 1u; + metadata.flags = imageModes[resource] << 1u; + if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; + plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)), metadata}); + } std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; if (binding.resources.size() > ShaderInfo::MaxSamplers) fail("prepared sampler mask capacity exceeded"); - for (std::uint32_t element = 0; element < binding.resources.size(); ++element) samplerMasks.at(binding.resources[element]) |= 1u << element; + for (std::uint32_t element = 0; element < binding.resources.size(); ++element) { + if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= 1u << element; + } } for (auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestImages) continue; @@ -496,6 +512,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con for (std::size_t element = 0; element < entry.resources.size(); ++element) { const auto& descriptor = snapshot.images.at(entry.resources[element]); if (descriptor.dwordCount != 8u) fail("invalid runtime image descriptor width"); + if (!entry.inactive.empty() && entry.inactive[element]) { + physical.guestDescriptor.insert(physical.guestDescriptor.end(), 8u, 0u); + continue; + } const auto offset = physical.guestDescriptor.size(); physical.guestDescriptor.insert(physical.guestDescriptor.end(), descriptor.dwords.begin(), descriptor.dwords.begin() + 8); if (entry.identityImageSwizzle[element]) physical.guestDescriptor[offset + 3u] = (physical.guestDescriptor[offset + 3u] & ~0xfffu) | ShaderImageIdentitySwizzle; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 5fda2ba0b..8978f1851 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -406,7 +406,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - auto bindings = bindingAllocator.Allocate(program, request.layout); + auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 1650714e3..7f726e995 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -7,6 +7,7 @@ namespace ShaderRecompiler { +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member); std::uint32_t ResourceForDescriptor(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource); std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t resultPtrType, std::uint32_t variableId, std::uint32_t arrayIndex, DescriptorBindingKind kind, std::uint32_t resource, const char* variableName); std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass numericClass); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 5e209712a..81806a48f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -4,6 +4,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include @@ -46,18 +47,8 @@ struct SampleSetup { std::uint32_t coord; }; -std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { - const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); - const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; -} - std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { - if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); + if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); return Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), descriptor, ConstantU32(state, component * 3u)), ConstantU32(state, 7u)); } @@ -1653,7 +1644,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.runtimeImageMetadata = 0u; return; } - const auto selector = base.indirectRoot == ImageResource::NoIndirectImage ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; const auto resultId = returnsValue ? ctx.Result(inst) : 0u; ctx.definitions.erase(&inst); @@ -1677,7 +1668,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { for (std::uint32_t index = 0u; index < modes.size(); ++index) { EmitLabel(state, labels[index]); std::uint32_t modeMerge = 0u; - if (base.indirectRoot != ImageResource::NoIndirectImage) { + if (base.indirectRoot != ImageResource::NoIndirectImage && !state.registrationModules) { const auto enabled = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageModeBase + memory.resource * PipelineSpecialization::ImageModeStride + index, 1u); const auto active = Binary(state, spv::OpINotEqual, TypeBool(state), enabled, ConstantU32(state, 0u)); const auto activeLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b1a37bb57..4989ed437 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -50,11 +50,28 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r } const auto pointer = state.module.AllocateId(); const bool heap = kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(kind) != ImageResourceClass::None; - const auto index = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); + const auto index = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); state.module.AddFunction(spv::OpAccessChain, resultPtrType, pointer, variableId, index); return pointer; } +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { + const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); + const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::uint32_t RuntimeMipCount(SpirvEmitterState& state, std::uint32_t resource) { + const auto word = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); + const auto first = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 12u)), 0xfu); + const auto last = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 16u)), 0xfu); + return Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), last, first), ConstantU32(state, 1u)); +} + std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource) { const bool storage = ImageBindingResourceClass(kind) == ImageResourceClass::Storage; const auto supported = [&](std::uint32_t capability) { @@ -240,6 +257,7 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std if (image.mipCount == 0u) { ExitDescriptorBindingFailure(state, kind, resource, "dynamic storage image has no mip descriptors"); } + const auto count = state.registrationModules ? RuntimeMipCount(state, resource) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto mergeLabel = state.module.AllocateId(); std::vector labels(image.mipCount); std::vector words{spv::OpSwitch, mipLod, mergeLabel}; @@ -252,7 +270,6 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std state.module.AddFunction(std::span(words)); for (std::uint32_t mip = 0; mip < image.mipCount; mip++) { EmitLabel(state, labels[mip]); - const auto count = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto active = Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mip), count); const auto writeLabel = state.module.AllocateId(); const auto nextLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c484fa2b8..cc7ab59a8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -398,7 +398,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto ArrayType = [&](std::uint32_t type) { const bool heap = binding.kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(binding.kind) != ImageResourceClass::None; const auto count = static_cast(binding.resources.size()); - const auto length = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); + const auto length = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); return state.module.Type(spv::OpTypeArray, type, length); }; switch (binding.kind) { From baf34374657f665ffcb05325fe8f83ee1e35837c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 13:53:14 +0200 Subject: [PATCH 03/19] perf(recompiler): read depth-compare state and export mappings at run time when modules are built at registration Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 4 +-- .../execution/BufferDescriptorRuntime.cpp | 8 +++-- .../tests/execution/DepthFastClearHarness.hpp | 5 +-- .../tests/execution/PreparedExports.cpp | 8 +++-- .../tests/execution/ProvokingVertex.cpp | 4 +-- .../tests/execution/RecordedKeptBytes.cpp | 4 +-- .../tests/execution/VertexFetch.cpp | 4 +-- .../IrMetadata/DescriptorBinding.hpp | 6 +++- .../Optimization/DescriptorBindingBuilder.hpp | 1 + .../Optimization/src/BindingAllocator.cpp | 1 + .../src/DescriptorBindingBuilder.cpp | 26 ++++++++++---- core/shader/recompiler/Recompiler.cpp | 35 ++++++++++++++++++- core/shader/recompiler/RuntimeAbi.hpp | 6 ++++ core/shader/recompiler/ShaderDiskCache.cpp | 2 ++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 16 +++++++-- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 16 files changed, 105 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 99955da54..29891d598 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -184,13 +184,13 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, + target.fixedPushSlots ? PixelPushLayout(meshPush, target) : ShaderRecompiler::BindingLayout{0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, std::nullopt }; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Mesh, &meshResult, 0}, - {ShaderStage::Fragment, &pixelResult, meshPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 1948f7e20..eafac530e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -73,6 +73,7 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { {"absent", 0, 128, false}, }}; const void* first = nullptr; + std::uint64_t variant = 0; for (const auto& test : cases) { Output.fill(Fill); const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; @@ -83,8 +84,11 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { runtime.userData = userData; const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); - if (first == nullptr) first = result->spirv.data(); - if (registrationModules) Require(result->spirv.data() == first, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); + if (first == nullptr) { + first = result->spirv.data(); + variant = result->PipelineVariantId(); + } + if (registrationModules) Require(result->spirv.data() == first && result->PipelineVariantId() == variant, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); for (std::uint32_t lane = 0; lane < Lanes; ++lane) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 18d3083b4..1e4e5bcb3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" #include #include #include @@ -96,7 +97,7 @@ inline Shaders Compile(AgcDriver::VulkanDevice& device) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, shaders.vertexPush, 128 - shaders.vertexPush} + PixelPushLayout(shaders.vertexPush, target) }; fragment.useCache = false; shaders.pixel = ShaderRecompiler::Recompile(fragment); @@ -107,7 +108,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil Pixels.fill(std::byte{0}); const std::array shaders{{ {ShaderStage::Vertex, &compiled.vertex, 0}, - {ShaderStage::Fragment, &compiled.pixel, compiled.vertexPush} + {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp index b0b85a7b7..bdbfd1362 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp @@ -29,7 +29,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { const std::array values = integer ? std::array{11, 22, 33, 44} : std::array{0, 0x3e800000u, 0x3f000000u, 0x3f800000u}; const std::array code{0x7e0002ffu, values[0], 0x7e0202ffu, values[1], 0x7e0402ffu, values[2], 0x7e0602ffu, values[3], 0xf800100fu, 0x03020100u, 0xf800183fu, 0x03020100u, 0xbf810000u}; AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, integer ? 0x7007u : 0x9009u}}; - RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), {0, 0, 0, 128}}; + RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), PixelPushLayout(0u, device.Target())}; const auto handle = PrepareShader(request); const auto& artifact = GetPreparedArtifact(*handle); std::uint32_t scalarType = 0; @@ -85,6 +85,10 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { request.useCache = cached; const auto materialized = MaterializeShader(request, *capture, *handle); Require(materialized->spirv.data() == pixel->spirv.data() && materialized->PipelineVariantId() == pixel->PipelineVariantId(), "export mapping did not reuse its specialized module"); + if (device.Target().registrationModules) { + Require(materialized->PipelineVariantId() == first->PipelineVariantId(), "registration modules: the export mapping specialized the fragment module"); + continue; + } for (std::size_t cursor = 5; cursor < materialized->spirv.size();) { const auto count = materialized->spirv[cursor] >> 16u; Require(count != 0u && count <= materialized->spirv.size() - cursor, "invalid specialized fragment instruction"); @@ -94,7 +98,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { } request.useCache = true; if (integer) continue; - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), 0}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), PixelPushOffset(0u, device.Target())}}}; pixels.fill(0xdeadbeef); otherPixels.fill(0xdeadbeef); device.Draw(state, draw, shaders); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp index f1651a590..2654a113b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp @@ -50,7 +50,7 @@ ShaderRecompiler::RecompileResult GuestPixel(const ShaderRecompiler::SpirvTarget request.context.waveSize = 64; request.context.pixel = pixel; request.target = target; - request.layout.pushConstantSizeBytes = 128; + request.layout = PixelPushLayout(0u, target); request.useCache = false; auto result = ShaderRecompiler::Recompile(request); result.variantId = 3; @@ -130,7 +130,7 @@ int main() { const auto guestPixel = GuestPixel(device->Target()); const std::array guestShaders{{ {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, - {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, 0} + {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, PixelPushOffset(0u, device->Target())} }}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, guestShaders, true, "guest P0"); std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, first/last/first"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp index a41fe4456..f6e0f04fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp @@ -200,13 +200,13 @@ void Draw(AgcDriver::VulkanDevice& device, GuestBuffer& pixels, GuestBuffer& ver {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp index 4baa2d05b..c42f5c35c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp @@ -85,9 +85,9 @@ void Run(AgcDriver::VulkanDevice& device) { pixelInfo.wave32 = true; pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); - ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index c1a9500b7..976a74f8e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -43,6 +43,7 @@ struct IrBindingLayout { std::uint32_t memoryOffsetCount = 0; bool dispatchThreadLimit = false; std::uint32_t runtimeImageCount = 0; + std::uint32_t runtimeStateDwords = 0; std::vector userDataRegisters; std::vector descriptors; @@ -52,9 +53,12 @@ struct IrBindingLayout { [[nodiscard]] std::uint32_t ImageMetadataDword() const { return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u); } - [[nodiscard]] std::uint32_t ShaderDataDwords() const { + [[nodiscard]] std::uint32_t RuntimeStateDword() const { return ImageMetadataDword() + runtimeImageCount * (sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)); } + [[nodiscard]] std::uint32_t ShaderDataDwords() const { + return RuntimeStateDword() + runtimeStateDwords; + } [[nodiscard]] bool UsesPushData() const { return pushDataStartDword != PushData::NoStart; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index d600e6fbb..6c15439dc 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -29,6 +29,7 @@ struct DescriptorBindingPlan { std::vector bindings; std::vector imageMetadata; std::vector specialization; + std::vector runtimeState; bool storageData = false; }; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 822cc38b7..268265461 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -103,6 +103,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules && (program.Resources().stage == IrShaderStage::Pixel || std::ranges::any_of(info.samplers, [](const auto& sampler) { return FoldsTexelOffsets(sampler); }))) next.runtimeStateDwords = RuntimeAbi::StateDwords; const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index bb333af47..851d92ed5 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -208,7 +208,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh return proof; } -std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata) { +std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata, std::span runtimeState) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); ++i) { const auto reg = layout.userDataRegisters[i]; @@ -226,6 +226,8 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st std::copy_n(descriptor.dwords.begin(), metadata.descriptor.size(), metadata.descriptor.begin()); std::memcpy(result.data() + entry.offset, &metadata, sizeof(metadata)); } + if (runtimeState.size() != layout.runtimeStateDwords) fail("runtime state does not match its shader data layout"); + std::copy(runtimeState.begin(), runtimeState.end(), result.begin() + layout.RuntimeStateDword()); return result; } @@ -255,8 +257,11 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; const auto unnormalized = ProveUnnormalized(info, snapshot); + std::uint32_t unnormalizedSamplers = 0u; for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { - if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + if (!FoldsTexelOffsets(info.samplers[index])) continue; + if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + else if (unnormalized.samplers[index]) unnormalizedSamplers |= 1u << index; } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { @@ -264,7 +269,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto compare = compareStates[index] = ResourceMaterializer::EmulatedCompareState(info, snapshot, index); const auto first = PipelineSpecialization::CompareBase + index * PipelineSpecialization::CompareWords; const std::array values{EmulatedCompare::Function(compare), (compare & EmulatedCompare::Linear) != 0u, EmulatedCompare::AddressX(compare), EmulatedCompare::AddressY(compare), EmulatedCompare::Reference(compare), (compare & EmulatedCompare::BorderWhite) != 0u}; - for (std::uint32_t word = 0; word < values.size(); ++word) plan.specialization.push_back({first + word, values[word]}); + for (std::uint32_t word = 0; word < values.size() && !registrationModules; ++word) plan.specialization.push_back({first + word, values[word]}); } std::vector imageModes(info.images.size()); for (std::size_t index = 0; index < info.images.size(); ++index) imageModes[index] = ResourceMaterializer::RuntimeImageMode(info.images[index], snapshot.images.at(index), info.runtimeImageModes.at(index)); @@ -301,7 +306,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); } - for (std::uint32_t target = 0; target < exportMappings.size(); ++target) { + for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); } std::vector samplerModes(info.samplers.size(), 0u); @@ -416,7 +421,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l metadata.binding = static_cast(compact.kind); metadata.firstElement = static_cast(element); metadata.elementCount = 1u; - metadata.flags = imageModes[resource] << 1u; + metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; const auto offset = layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)); plan.imageMetadata.push_back({resource, offset, metadata}); @@ -447,10 +452,16 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l RuntimeAbi::ResourceMetadata metadata{}; if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width"); metadata.elementCount = 1u; - metadata.flags = imageModes[resource] << 1u; + if (imageModes[resource] > RuntimeAbi::ImageModeMask) fail("runtime image mode exceeds its metadata field"); + metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)), metadata}); } + if (layout.runtimeStateDwords != 0u) { + plan.runtimeState.assign(layout.runtimeStateDwords, 0u); + plan.runtimeState.at(RuntimeAbi::StateUnnormalizedSamplers) = unnormalizedSamplers; + for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); + } std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; @@ -488,13 +499,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Select(const DescriptorBindingPl } } selected.imageMetadata = plan.imageMetadata; + selected.runtimeState = plan.runtimeState; return selected; } void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const auto& layout = compiled.layout; const bool needsShaderData = layout.UsesPushData() || plan.storageData; - auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata) : std::vector{}; + auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector{}; std::vector bindings; bindings.reserve(plan.bindings.size()); std::size_t writtenHere = 0; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 8978f1851..f7d2ff5b2 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -12,6 +12,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "ShaderDiskCache.hpp" #include +#include #include #include #include @@ -602,6 +603,38 @@ struct PreparedBindingPlan { std::map, std::shared_ptr> modules; }; +struct RegistrationModuleEntry { + std::once_flag ready; + std::exception_ptr failure; + std::shared_ptr module; +}; + +std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { + std::vector key{variantId}; + key.insert(key.end(), moduleKey.begin(), moduleKey.end()); + static std::shared_mutex mutex; + static std::map, std::shared_ptr> modules; + std::shared_ptr entry; + { + std::shared_lock lock(mutex); + if (const auto found = modules.find(key); found != modules.end()) entry = found->second; + } + if (entry == nullptr) { + std::unique_lock lock(mutex); + const auto found = modules.find(key); + entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; + } + std::call_once(entry->ready, [&] { + try { + entry->module = specialize(); + } catch (...) { + entry->failure = std::current_exception(); + } + }); + if (entry->failure) std::rethrow_exception(entry->failure); + return entry->module; +} + std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool registrationModules) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); @@ -769,7 +802,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = specializeModule(variant.artifact, vertexClasses, constants, request.target); + const auto module = request.target.registrationModules && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 131372e68..8d5019b93 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -39,6 +39,12 @@ inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; +inline constexpr std::uint32_t StateUnnormalizedSamplers = 0u; +inline constexpr std::uint32_t StateExportMappings = 1u; +inline constexpr std::uint32_t StateDwords = 3u; +inline constexpr std::uint32_t ImageModeMask = 0xffu; +inline constexpr std::uint32_t CompareStateShift = 9u; +inline constexpr std::uint32_t CompareStateMask = 0x1fffu; struct ResourceMetadata { std::uint32_t binding; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 203770688..0949aef04 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -416,6 +416,7 @@ void encodeLayout(Writer& writer, const IrBindingLayout& layout) { writer.Value(layout.memoryOffsetCount); writer.Value(layout.dispatchThreadLimit); writer.Value(layout.runtimeImageCount); + writer.Value(layout.runtimeStateDwords); writer.Values(std::span(layout.userDataRegisters)); writer.List(layout.descriptors, [](Writer& out, const IrDescriptorBinding& descriptor) { out.Value(descriptor.kind); @@ -429,6 +430,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) { reader.Value(layout.memoryOffsetCount); reader.Value(layout.dispatchThreadLimit); reader.Value(layout.runtimeImageCount); + reader.Value(layout.runtimeStateDwords); reader.Values(layout.userDataRegisters); reader.List(layout.descriptors, 12, [](Reader& in, IrDescriptorBinding& descriptor) { in.Value(descriptor.kind); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 81806a48f..7297e528d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -47,6 +47,11 @@ struct SampleSetup { std::uint32_t coord; }; +std::uint32_t RuntimeImageMode(SpirvEmitterState& state, std::uint32_t resource) { + const auto mode = Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + return state.registrationModules ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), mode, ConstantU32(state, RuntimeAbi::ImageModeMask)) : mode; +} + std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); @@ -1264,7 +1269,12 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess auto& state = ctx.state; const auto& mem = access.mem; const auto& image = access.image; - const auto parameter = [&](std::uint32_t word) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); }; + const auto compare = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, mem.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, RuntimeAbi::CompareStateShift)), RuntimeAbi::CompareStateMask) : 0u; + const auto parameter = [&](std::uint32_t word) { + if (!state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); + constexpr std::array, 6> fields{{{EmulatedCompare::FunctionShift, 7u}, {4u, 1u}, {EmulatedCompare::ClampXShift, 3u}, {EmulatedCompare::ClampYShift, 3u}, {EmulatedCompare::ReferenceShift, 3u}, {12u, 1u}}}; + return EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), compare, ConstantU32(state, fields.at(word).first)), fields.at(word).second); + }; const auto equal = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpIEqual, TypeBool(state), value, ConstantU32(state, literal)); }; if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented"); if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); @@ -1484,7 +1494,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con } else if (!offsetOperand) { sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); } else { - const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); + const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateUnnormalizedSamplers), ConstantU32(state, mem.sampler)), 1u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); const auto foldLabel = state.module.AllocateId(); const auto operandLabel = state.module.AllocateId(); @@ -1644,7 +1654,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.runtimeImageMetadata = 0u; return; } - const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : RuntimeImageMode(state, memory.resource); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; const auto resultId = returnsValue ? ctx.Result(inst) : 0u; ctx.definitions.erase(&inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1793ea2f8..ac2cc1366 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1319,7 +1319,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (exp.index >= 8u) throw std::runtime_error("fragment export target exceeds the runtime mapping table"); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { - const auto index = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); + const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u + component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); components[component] = state.module.AllocateId(); state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], value, index); } From 4d0b5c78984fbaa620acd39985e623ca8459cb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 14:50:51 +0200 Subject: [PATCH 04/19] perf(agc): give the fragment stage its own descriptor set when modules are built at registration Co-Authored-By: Claude --- .../Graphics/include/PipelineLibrary.hpp | 2 + .../Graphics/include/ShaderResources.hpp | 16 +++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 33 +++++- .../Graphics/src/PipelineLibrary.cpp | 27 +++-- .../Graphics/src/ShaderResources.cpp | 111 ++++++++++++++---- .../Graphics/src/ShaderValidation.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- .../Optimization/src/BindingAllocator.cpp | 2 +- .../src/DescriptorBindingBuilder.cpp | 8 +- core/shader/recompiler/RuntimeAbi.hpp | 6 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 2 +- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- 13 files changed, 164 insertions(+), 53 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index f480042a3..4ffb5e3bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -19,6 +19,8 @@ struct PipelineLibraryKeys { std::vector fragmentOutput; std::vector renderPass; std::vector layout; + std::vector preRasterizationLayout; + std::vector fragmentLayout; }; std::span PipelineLibraryDynamicStates(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 014a06035..4e4cf13b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -117,6 +117,8 @@ public: std::vector LayoutBindings() const; bool LayoutUpdateAfterBind() const; std::span LayoutBindingFlags() const { return layoutBindingFlags; } + VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; } + bool SplitSets() const { return splitSets; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; @@ -127,6 +129,7 @@ public: std::shared_ptr buffer; }; DescriptorCache* cache = nullptr; + DescriptorCache::SetAllocation fragmentAllocation; DescriptorCache::SetAllocation allocation; std::vector snapshots; ~DrawBindings(); @@ -165,6 +168,7 @@ public: bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; const std::vector& LayoutKey() const { return layoutKey; } + const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. std::string Describe() const; @@ -428,6 +432,17 @@ private: VkDescriptorPool pool = VK_NULL_HANDLE; // The cache pool the set was allocated from, freed back to it on release. DescriptorCache::SetAllocation cacheAllocation; + struct SetObjects { + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + bool ownsLayout = false; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + DescriptorCache::SetAllocation cacheAllocation; + }; + bool splitSets = false; + std::vector fragmentLayoutKey; + SetObjects fragment; + std::uint32_t setOf(const Binding& binding) const; std::vector allocations; std::vector descriptorBuffers; std::vector descriptorImages; @@ -479,6 +494,7 @@ private: // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; std::vector layoutBindingFlags; + std::vector fragmentBindingFlags; std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 2f3bf711d..914e7b269 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1764,7 +1764,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); if (drawBindings != nullptr) { const auto set = drawBindings->allocation.set; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + const auto fragmentSet = drawBindings->fragmentAllocation.set; + if (set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + if (fragmentSet != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragmentSet, 0, nullptr); } else { resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 139cff586..2bcd134d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -217,8 +217,28 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); - appendKey(keys.layout, pushStagesOf(context)); - for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + if (!resources.SplitSets()) { + appendKey(keys.layout, pushStagesOf(context)); + for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + keys.preRasterizationLayout = keys.fragmentLayout = keys.layout; + return true; + } + const auto splitKey = [&](std::vector& key, std::uint32_t sets) { + appendKey(key, ~0u); + appendKey(key, sets); + appendKey(key, pushStagesOf(context)); + if ((sets & 1u) != 0u) { + appendKey(key, resources.LayoutKey().size()); + for (const auto word : resources.LayoutKey()) appendKey(key, word); + } + if ((sets & 2u) != 0u) { + appendKey(key, resources.FragmentLayoutKey().size()); + for (const auto word : resources.FragmentLayoutKey()) appendKey(key, word); + } + }; + splitKey(keys.layout, 3u); + splitKey(keys.preRasterizationLayout, 1u); + splitKey(keys.fragmentLayout, 2u); return true; } @@ -280,12 +300,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const auto setLayout = resources.Layout(); + const std::array setLayouts{resources.Layout(), resources.FragmentLayout()}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = 1; - layoutInfo.pSetLayouts = &setLayout; + layoutInfo.flags = resources.SplitSets() ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; + layoutInfo.setLayoutCount = resources.SplitSets() ? 2u : 1u; + layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout graphics"); @@ -709,6 +730,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat } append(key, resources.LayoutKey().size()); for (const auto word : resources.LayoutKey()) append(key, word); + append(key, resources.FragmentLayoutKey().size()); + for (const auto word : resources.FragmentLayoutKey()) append(key, word); append(key, state.hasColorTarget); append(key, state.rectList); append(key, state.topology); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index edb90c69e..df499bb21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -111,12 +112,22 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip device.destroyLayout = context.Function("vkDestroyPipelineLayout"); device.destroyModule = context.Function("vkDestroyShaderModule"); } - auto pipelineLayout = device.layouts.find(keys.layout); - if (pipelineLayout == device.layouts.end()) { + const auto layoutFor = [&](const Key& key, std::optional set) { + auto found = device.layouts.find(key); + if (found != device.layouts.end()) return found->second; + auto info = layout; + std::array sets{VK_NULL_HANDLE, VK_NULL_HANDLE}; + if (set) { + sets.at(*set) = layout.pSetLayouts[*set]; + info.pSetLayouts = sets.data(); + } VkPipelineLayout handle = VK_NULL_HANDLE; - Check(context.Function("vkCreatePipelineLayout")(context.device, &layout, nullptr, &handle), "vkCreatePipelineLayout library"); - pipelineLayout = device.layouts.emplace(keys.layout, handle).first; - } + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library"); + return device.layouts.emplace(key, handle).first->second; + }; + const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; + const auto fullLayout = layoutFor(keys.layout, std::nullopt); + const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, 0u) : fullLayout, split ? layoutFor(keys.fragmentLayout, 1u) : fullLayout, VK_NULL_HANDLE}; std::vector preRasterization; const VkPipelineShaderStageCreateInfo* fragment = nullptr; bool mesh = false; @@ -135,7 +146,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip }; const std::array partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput}; const Key none; - const std::array contextKeys{&none, &keys.layout, &keys.layout, &keys.renderPass}; + const std::array contextKeys{&none, &keys.preRasterizationLayout, &keys.fragmentLayout, &keys.renderPass}; const VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; VkPipelineDepthStencilStateCreateInfo dynamicDepth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; static const bool backgroundLinking = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; @@ -155,7 +166,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; create.pDynamicState = info.pDynamicState; - create.layout = pipelineLayout->second; + create.layout = partLayouts[part]; switch (flags[part]) { case VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT: create.pVertexInputState = info.pVertexInputState; @@ -212,7 +223,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip *optimized = slot; const auto createPipelines = context.Function("vkCreateGraphicsPipelines"); const auto cache = context.pipelineCache; - const auto libraryLayout = pipelineLayout->second; + const auto libraryLayout = fullLayout; enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6c1d10286..e223a6d45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1166,6 +1166,14 @@ VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span PoolSizes(std::span bindings) { + std::map counts; + for (const auto& binding : bindings) counts[binding.descriptorType] += binding.descriptorCount; + std::vector sizes; + for (const auto& [type, count] : counts) sizes.push_back({type, count}); + return sizes; +} + VkDescriptorPool CreateSetPool(const Context& context, std::span sizes, bool updateAfterBind) { VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; info.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; @@ -1339,7 +1347,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons updateAfterBind = true; }; for (const auto& binding : shader.program->bindings) { - Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); + Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set"); Require(occupied.insert(binding.binding).second, "duplicate shader binding"); const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer; const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds; @@ -1416,23 +1424,62 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions(); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - std::vector description; + splitSets = std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }); + std::array, 2> descriptions; for (const auto& binding : bindings) { - description.push_back(binding.layout); - layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); - layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + const auto set = setOf(binding); + descriptions[set].push_back(binding.layout); + (set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + auto& key = set == 0 ? layoutKey : fragmentLayoutKey; + key.insert(key.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + } + if (updateAfterBind) { + layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + if (splitSets) fragmentLayoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); } - if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; - if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags); - } else { + const auto createLayout = [&](const std::vector& key, const std::vector& description, const std::vector& flags, bool& owns) { + if (context.descriptorCache != nullptr && !noLayoutCache) return context.descriptorCache->Layout(key, description, updateAfterBind, flags); ValidateDescriptorLayout(context, description, updateAfterBind); - _layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags); - ownsLayout = true; + owns = true; + return CreateSetLayout(context, description, updateAfterBind, flags); + }; + _layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout); + if (splitSets) { + fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); + if (!descriptions[1].empty()) { + const auto sizes = PoolSizes(descriptions[1]); + if (context.descriptorCache != nullptr && !noPoolCache) { + fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind); + fragment.set = fragment.cacheAllocation.set; + } + if (fragment.set == VK_NULL_HANDLE) { + fragment.pool = CreateSetPool(context, sizes, updateAfterBind); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = fragment.pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &fragment.layout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &fragment.set), "vkAllocateDescriptorSets"); + } + } } - if (!bindings.empty()) { + if (splitSets && !descriptions[0].empty()) { + const auto sizes = PoolSizes(descriptions[0]); + if (context.descriptorCache != nullptr && !noPoolCache) { + cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); + _set = cacheAllocation.set; + } + if (_set == VK_NULL_HANDLE) { + pool = CreateSetPool(context, sizes, updateAfterBind); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &_layout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); + } + } + if (!splitSets && !bindings.empty()) { // The set is sized from the plan: every image element becomes exactly one descriptor // when stage B looks it up. std::vector sizes; @@ -1492,7 +1539,7 @@ void ShaderResources::buildComplete() { if (usesBda) bda = std::make_unique(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); phase(BuildPhase::Bda); - if (_set != VK_NULL_HANDLE) { + if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { // One update call for the whole set: the info arrays are sized up front so every write's // pointer into them stays valid until the call. std::size_t bufferCount = 0; @@ -1514,7 +1561,7 @@ void ShaderResources::buildComplete() { }; for (const auto& binding : bindings) { VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = _set; + write.dstSet = setOf(binding) == 0 ? _set : fragment.set; write.dstBinding = binding.layout.binding; write.descriptorCount = binding.layout.descriptorCount; write.descriptorType = binding.layout.descriptorType; @@ -3334,10 +3381,19 @@ void ShaderResources::release() noexcept { if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation); if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (_layout && ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr); + if (fragment.cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(fragment.cacheAllocation); + if (fragment.pool) context.Function("vkDestroyDescriptorPool")(context.device, fragment.pool, nullptr); + if (fragment.layout && fragment.ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, fragment.layout, nullptr); cacheAllocation = {}; pool = VK_NULL_HANDLE; _set = VK_NULL_HANDLE; _layout = VK_NULL_HANDLE; + fragment = {}; +} + +std::uint32_t ShaderResources::setOf(const Binding& binding) const { + constexpr auto count = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + return splitSets && binding.layout.binding / count == static_cast(ShaderRecompiler::RuntimeAbi::Stage::Fragment) ? 1u : 0u; } VkDescriptorSetLayout ShaderResources::Layout() const { @@ -3356,11 +3412,12 @@ bool ShaderResources::LayoutUpdateAfterBind() const { ShaderResources::DrawBindings::~DrawBindings() { if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); + if (cache != nullptr && fragmentAllocation.set != VK_NULL_HANDLE) cache->Free(fragmentAllocation); } std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { std::vector moved; - if (_set == VK_NULL_HANDLE || usesBda) return moved; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return moved; for (const auto& shader : shaders) { if (shader.program == nullptr) return std::nullopt; for (const auto& binding : shader.program->bindings) { @@ -3413,7 +3470,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { } std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { - if (_set == VK_NULL_HANDLE || usesBda) return {}; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); std::size_t totalSnapshotBytes = 0; @@ -3460,13 +3517,17 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } if (selected.empty()) return {}; Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache"); - std::map counts; - for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount; - std::vector sizes; - for (const auto& [type, count] : counts) sizes.push_back({type, count}); + std::array, 2> descriptions; + for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout); result->cache = context.descriptorCache; - result->allocation = result->cache->Allocate(_layout, sizes); - Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + if (!descriptions[0].empty()) { + result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0])); + Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + } + if (!descriptions[1].empty()) { + result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1])); + Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + } auto buffers = descriptorBuffers; auto writes = descriptorWrites; std::vector snapshotIndices(allocations.size(), selected.size()); @@ -3474,7 +3535,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi for (std::size_t index = 0; index < bindings.size(); ++index) { const auto& binding = bindings[index]; auto& write = writes[index]; - write.dstSet = result->allocation.set; + write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set; if (binding.allocations.empty()) continue; const auto offset = static_cast(write.pBufferInfo - descriptorBuffers.data()); write.pBufferInfo = buffers.data() + offset; @@ -3492,8 +3553,8 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const { - if (_set == VK_NULL_HANDLE) return; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (_set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (fragment.set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragment.set, 0, nullptr); } namespace { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index d30076237..d6e0fbf69 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -574,7 +574,7 @@ std::set ValidateShaders(std::span shaders, const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); - for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); + for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); if (i != 0) { for (const auto& [location, signature] : current.inputs) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..29e5122ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4388,7 +4388,7 @@ void validationTests() { { auto vertex = pushed({.push = true, .bufferArray = 2}); vertex.bindings.front().descriptorSet = 1; - expectFailure([&] { validate(vertex, 8); }, "descriptor set other than zero"); + expectFailure([&] { validate(vertex, 8); }, "descriptor set other than its stage's"); } { auto vertex = pushed({.push = true, .bufferArray = 2}); diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 268265461..3f58c1398 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -103,7 +103,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); - if (registrationModules && (program.Resources().stage == IrShaderStage::Pixel || std::ranges::any_of(info.samplers, [](const auto& sampler) { return FoldsTexelOffsets(sampler); }))) next.runtimeStateDwords = RuntimeAbi::StateDwords; + if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords; const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 851d92ed5..b76a88dce 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -257,11 +257,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; const auto unnormalized = ProveUnnormalized(info, snapshot); - std::uint32_t unnormalizedSamplers = 0u; for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { - if (!FoldsTexelOffsets(info.samplers[index])) continue; - if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); - else if (unnormalized.samplers[index]) unnormalizedSamplers |= 1u << index; + if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { @@ -382,7 +379,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; - physical.descriptorSet = RuntimeAbi::DescriptorSet; + physical.descriptorSet = registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; physical.binding = NativeBinding(stage, compact.kind); physical.count = compact.resources.empty() ? 1u : static_cast(compact.resources.size()); physical.kind = PhysicalKindFor(compact.kind); @@ -459,7 +456,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } if (layout.runtimeStateDwords != 0u) { plan.runtimeState.assign(layout.runtimeStateDwords, 0u); - plan.runtimeState.at(RuntimeAbi::StateUnnormalizedSamplers) = unnormalizedSamplers; for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); } std::vector samplerMasks(info.samplers.size()); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 8d5019b93..2a303c504 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -11,6 +11,7 @@ namespace ShaderRecompiler::RuntimeAbi { inline constexpr std::uint32_t Version = 11u; inline constexpr std::uint32_t DescriptorSet = 0u; +inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; inline constexpr std::uint32_t PushConstantDwords = 32u; inline constexpr std::uint32_t FirstImageBinding = 1u; @@ -39,9 +40,8 @@ inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; -inline constexpr std::uint32_t StateUnnormalizedSamplers = 0u; -inline constexpr std::uint32_t StateExportMappings = 1u; -inline constexpr std::uint32_t StateDwords = 3u; +inline constexpr std::uint32_t StateExportMappings = 0u; +inline constexpr std::uint32_t StateDwords = 2u; inline constexpr std::uint32_t ImageModeMask = 0xffu; inline constexpr std::uint32_t CompareStateShift = 9u; inline constexpr std::uint32_t CompareStateMask = 0x1fffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 7297e528d..d380b33fc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1494,7 +1494,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con } else if (!offsetOperand) { sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); } else { - const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateUnnormalizedSamplers), ConstantU32(state, mem.sampler)), 1u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); + const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); const auto foldLabel = state.module.AllocateId(); const auto operandLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index cc7ab59a8..4227e8d83 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -391,7 +391,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) { const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage); state.module.AddName(variable, name); - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind)); return variable; }; From d7e0f27d6b602413d3b728ab581982d63ae6d8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 15:18:38 +0200 Subject: [PATCH 05/19] perf(agc): describe pipeline set layouts from the compiled shaders alone Co-Authored-By: Claude --- .../Graphics/include/Pipeline.hpp | 9 +++++ .../Graphics/include/ShaderResources.hpp | 9 +++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 +++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 38 +++++++++++-------- .../Graphics/src/ShaderResources.cpp | 38 +++++++++++++++++++ 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 0b97a08f6..ee0f2d094 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -41,6 +41,7 @@ public: // before, during and after the pass (GENERAL for resident targets, which then need no // transitions). Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ~Pipeline(); Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; @@ -98,6 +99,14 @@ private: // only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per // draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +struct DescribedSetLayouts { + PipelineSetLayouts layouts; + std::vector mainKey; + std::vector fragmentKey; + PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; } +}; +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders); // Destroys the cached pipelines of a device; to be called before the device goes away. Without it, // entries of a gone device are recognised by their buffer pool (made and reset with the device, so // it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 4e4cf13b4..7b29f6df0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -51,6 +51,14 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres // a storage image other than `except` has results pending in [address, address + bytes). bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except); +struct PipelineSetLayouts { + VkDescriptorSetLayout main = VK_NULL_HANDLE; + VkDescriptorSetLayout fragment = VK_NULL_HANDLE; + bool split = false; + std::span mainKey; + std::span fragmentKey; +}; + class DescriptorCache { public: explicit DescriptorCache(const Context& context); @@ -119,6 +127,7 @@ public: std::span LayoutBindingFlags() const { return layoutBindingFlags; } VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; } bool SplitSets() const { return splitSets; } + PipelineSetLayouts SetLayouts() const { return {_layout, fragment.layout, splitSets, layoutKey, fragmentLayoutKey}; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 914e7b269..946f2a560 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2118,6 +2118,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT; } const State& pipelineState = masked.has_value() ? *masked : state; + static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr; + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = DescribeSetLayouts(context, shaders); + const auto built = resources->SetLayouts(); + Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's"); + } auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // Resident targets' views are stable while their storage image lives, so the framebuffer is // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 2bcd134d1..aaff3c414 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -163,7 +163,7 @@ void appendKey(std::vector& key, const TValue& value) { key.insert(key.end(), bytes.begin(), bytes.end()); } -bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders) { +bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; keys = {}; @@ -217,9 +217,9 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); - if (!resources.SplitSets()) { + if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); - for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + for (const auto word : resources.mainKey) appendKey(keys.layout, word); keys.preRasterizationLayout = keys.fragmentLayout = keys.layout; return true; } @@ -228,12 +228,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(key, sets); appendKey(key, pushStagesOf(context)); if ((sets & 1u) != 0u) { - appendKey(key, resources.LayoutKey().size()); - for (const auto word : resources.LayoutKey()) appendKey(key, word); + appendKey(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) appendKey(key, word); } if ((sets & 2u) != 0u) { - appendKey(key, resources.FragmentLayoutKey().size()); - for (const auto word : resources.FragmentLayoutKey()) appendKey(key, word); + appendKey(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) appendKey(key, word); } }; splitKey(keys.layout, 3u); @@ -244,7 +244,9 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : Pipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout) {} + +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); sampleLocationsEnabled = ValidateSampleLocations(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep @@ -300,12 +302,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const std::array setLayouts{resources.Layout(), resources.FragmentLayout()}; + const std::array setLayouts{resources.main, resources.fragment}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.flags = resources.SplitSets() ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; - layoutInfo.setLayoutCount = resources.SplitSets() ? 2u : 1u; + layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; + layoutInfo.setLayoutCount = resources.split ? 2u : 1u; layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; @@ -696,7 +698,7 @@ void append(std::vector& key, const TValue& value) { // (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list // control and evaluation stages are generated from the vertex and fragment results, which the key // already names, so they carry no id of their own. -void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { +void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { using Stage = ShaderRecompiler::ShaderStage; key.clear(); append(key, context.device); @@ -728,10 +730,10 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, attribute.format); append(key, attribute.offset); } - append(key, resources.LayoutKey().size()); - for (const auto word : resources.LayoutKey()) append(key, word); - append(key, resources.FragmentLayoutKey().size()); - for (const auto word : resources.FragmentLayoutKey()) append(key, word); + append(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) append(key, word); + append(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) append(key, word); append(key, state.hasColorTarget); append(key, state.rectList); append(key, state.topology); @@ -851,6 +853,10 @@ void reportPipelines(PipelineStore& store) { } std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { + return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout); +} + +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr; if (disabled) return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e223a6d45..7bee0f6a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include #include @@ -1512,6 +1513,43 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders) { + Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache"); + DescribedSetLayouts result; + std::array, 2> descriptions; + bool split = false; + bool updateAfterBind = false; + std::uint64_t storageBuffers = 0; + for (const auto& shader : shaders) { + Require(shader.program != nullptr, "missing compiled shader"); + const VkShaderStageFlags flags = VulkanStage(shader.stage); + std::uint64_t stageStorageBuffers = 0; + std::uint64_t stageResources = 0; + for (const auto& binding : shader.program->bindings) { + split |= binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; + const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; + const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER; + if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count; + if (type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) { + stageStorageBuffers += binding.count; + storageBuffers += binding.count; + } + if (stageStorageBuffers > context.limits.maxPerStageDescriptorStorageBuffers || stageResources > context.limits.maxPerStageResources) updateAfterBind = true; + descriptions[binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet ? 1 : 0].push_back({binding.binding, type, binding.count, flags, nullptr}); + } + } + if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) updateAfterBind = true; + for (std::size_t set = 0; set < descriptions.size(); ++set) { + auto& key = set == 0 ? result.mainKey : result.fragmentKey; + for (const auto& binding : descriptions[set]) key.insert(key.end(), {binding.binding, static_cast(binding.descriptorType), binding.descriptorCount, binding.stageFlags}); + if (updateAfterBind && (set == 0 || split)) key.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + } + result.layouts.split = split; + result.layouts.main = context.descriptorCache->Layout(result.mainKey, descriptions[0], updateAfterBind); + if (split) result.layouts.fragment = context.descriptorCache->Layout(result.fragmentKey, descriptions[1], updateAfterBind); + return result; +} + void ShaderResources::buildComplete() { PerformanceTimer frameTiming("Resources.Complete"); Require(!completed, "cannot update a published descriptor heap"); From 7f64d14772400e83bc24fc1b75615133bc874271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 15:51:30 +0200 Subject: [PATCH 06/19] perf(agc): build the shader pipeline libraries when graphics stages are resolved at registration Co-Authored-By: Claude --- .../include/Driver/Draw/DrawCache.hpp | 3 ++ .../Execution/include/VulkanDevice.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 30 +++++++++++++++++-- .../Execution/src/VulkanDevice.cpp | 19 ++++++++++++ .../Graphics/include/Pipeline.hpp | 1 + .../Graphics/include/PipelineLibrary.hpp | 2 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 25 +++++++++++++++- .../Graphics/src/PipelineLibrary.cpp | 15 ++++++++-- .../tests/execution/PreparedGraphics.cpp | 17 +++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../include/Optimization/ResourceProgram.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 12 ++++++++ 12 files changed, 120 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 06d954d3b..5fa283462 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -37,6 +37,9 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const struct PreparedGraphicsStage { std::shared_ptr snapshot; PreparedShaders::Entry entry; + ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex; + std::uint32_t pushOffset = 0; + std::shared_ptr registration; }; std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index fe9ea5117..2027874af 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -299,6 +299,7 @@ public: }; IndirectDrawSupport DrawIndirectSupport() const; void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); + bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders); private: VulkanDevice(const PresentationWindow* window, bool compileOnly); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 580d119af..ed72dcd0d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,8 @@ namespace AgcDriver::DriverDetail { std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { try { - return ShaderRecompiler::PrepareShader(request, preparation); + auto handle = ShaderRecompiler::PrepareShader(request, preparation); + return handle; } catch (...) { try { if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast(Driver::dumpRequest(request.shader.codeAddress, request)); @@ -860,7 +862,14 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } const auto bytes = handle->artifact->bindings.pushConstantSizeBytes; require(bytes <= room, "prepared graphics stages exceed the push constant block"); - prepared.push_back({program.snapshot, {program.codeOffset, handle}}); + std::shared_ptr registration; + if (target.registrationModules) { + try { + registration = ShaderRecompiler::RegistrationResult(request, *handle); + } catch (const std::exception&) { + } + } + prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)}); pushOffset += bytes; } transaction.Commit(); @@ -930,6 +939,23 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std if (stage.snapshot->type == 1) continue; ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); } + if (localDevice->Target().registrationModules && !prepared.empty() && std::ranges::all_of(prepared, [](const auto& stage) { return stage.registration != nullptr && (stage.stage == ShaderRecompiler::ShaderStage::Vertex || stage.stage == ShaderRecompiler::ShaderStage::Fragment); }) && prepared.back().stage == ShaderRecompiler::ShaderStage::Fragment) { + std::vector> results; + std::vector> stages; + for (const auto& stage : prepared) { + results.push_back(stage.registration); + stages.emplace_back(stage.stage, stage.pushOffset); + } + SubmitRegistrationPreparation([localDevice, results = std::move(results), stages = std::move(stages), shaderStages = decoded.state.stages] { + std::vector shaders; + for (std::size_t index = 0; index < results.size(); ++index) shaders.push_back({stages[index].first, results[index].get(), stages[index].second}); + try { + static_cast(localDevice->PrebuildGraphicsLibraries(shaderStages, shaders)); + } catch (const std::exception& error) { + APS5_LOG_OUT("graphics library prebuild skipped: %s", error.what()); + } + }); + } PreparedShaderState::GraphicsAbi abi{key, registry, {}}; for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot); auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 858e855dc..78d96fe75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3058,6 +3058,25 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) { Graphics::RunColorMetadataPass(graphicsContext(), pass); } +bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders) { + PerformanceTimer timing("Vulkan.GraphicsPipelinePrebuild"); + const auto context = graphicsContext(); + if (!context.graphicsPipelineLibrary || context.descriptorCache == nullptr) return false; + Graphics::State state{}; + state.stages = stages; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.color.exportIndex = 0; + state.colors.push_back(state.color); + state.hasColorTarget = true; + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + state.renderExtent = {1, 1}; + const auto layouts = Graphics::DescribeSetLayouts(context, shaders); + return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), shaders); +} + void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { for (const auto& shader : shaders) { require(shader.program != nullptr, "missing compiled shader"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index ee0f2d094..f3a48a189 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -43,6 +43,7 @@ public: Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ~Pipeline(); + static bool Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders); Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index 4ffb5e3bc..5541197b2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -33,7 +33,7 @@ struct OptimizedPipeline { void Release() noexcept; }; -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}); +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}, bool partsOnly = false); void WaitForOptimizedPipelines(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index aaff3c414..911f1a10a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -152,6 +152,9 @@ bool ShadersWriteMemory(std::span shaders) { namespace { +thread_local bool prebuilding = false; +thread_local bool prebuilt = false; + VkShaderStageFlags pushStagesOf(const Context& context) { return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); } @@ -469,7 +472,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.renderPass = renderPass; if (dynamicRendering) pipelineInfo.pNext = &rendering; timing.Mark("modules_and_state"); - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); + if (prebuilding && !libraries) { + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + return; + } + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, prebuilding); + if (prebuilding) { + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + prebuilt = true; + return; + } else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); @@ -489,6 +503,15 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } } +bool Pipeline::Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders) { + struct Scope { + Scope() { prebuilding = true; prebuilt = false; } + ~Scope() { prebuilding = false; } + } scope; + const Pipeline pipeline(context, state, vertexInput, resources, shaders); + return prebuilt; +} + Pipeline::~Pipeline() { release(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index df499bb21..a6bbdb1cb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -102,10 +102,10 @@ std::span PipelineLibraryDynamicStates() { return dynamicStates; } -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules) { +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules, bool partsOnly) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); auto& store = Libraries(); - std::lock_guard lock(store.mutex); + std::unique_lock lock(store.mutex); auto& device = store.devices[context.device]; if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); @@ -193,7 +193,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip break; } VkPipeline handle = VK_NULL_HANDLE; - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle), "vkCreateGraphicsPipelines library"); + if (partsOnly) lock.unlock(); + const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + if (partsOnly) lock.lock(); + Check(created, "vkCreateGraphicsPipelines library"); + if (const auto raced = cache.find(key); raced != cache.end()) { + device.destroyPipeline(context.device, handle, nullptr); + libraries.push_back(raced->second); + continue; + } cache.emplace(key, handle); if (optimize) { for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { @@ -207,6 +215,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip ++device.counters.built[part]; libraries.push_back(handle); } + if (partsOnly) return VK_NULL_HANDLE; PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 5d456afdf..7b4970657 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "VulkanTestDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "SceShaders.hpp" @@ -130,6 +131,22 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path } const auto stages = PrepareGraphicsStages(prepared, target); Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages"); + if (!mesh && !tessellation && target.registrationModules) { + const auto prebuild = [&](const std::vector& group) { + Require(group.front().registration != nullptr && group.back().registration != nullptr, "registration modules: a prepared stage has no registration result"); + const std::array shaders{{{group.front().stage, group.front().registration.get(), group.front().pushOffset}, {group.back().stage, group.back().registration.get(), group.back().pushOffset}}}; + const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt"); + const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + return std::pair{after.built[1] - before.built[1] + after.built[2] - before.built[2], after.linked - before.linked}; + }; + const auto [first, firstLinks] = prebuild(stages); + Require(first == 2u && firstLinks == 0u, "registration modules: the prebuild did not build exactly the two shader libraries without linking"); + const auto again = PrepareGraphicsStages(prepared, target); + Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object"); + const auto [second, secondLinks] = prebuild(again); + Require(second == 0u && secondLinks == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); + } for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle); Require(!pixelArtifact.fragmentParameters.empty() && pixelArtifact.fragmentParameters.front().sourceLocation == 3u, "prepared fragment lost interpolant mapping"); diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 88aafc8fe..6d3d68e2d 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -236,6 +236,7 @@ function(add_shader_recompiler target runtimeTarget) MatchesPreparedShader GetPreparedArtifact MaterializeShader + RegistrationResult ResolveSource ResourceMaterializer::SpecializationNanoseconds RuntimeAbi::RequireVersion diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index f23d1b8f6..909f3cea1 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -70,6 +70,7 @@ private: void BuildPreparedShaderKey(const RecompileRequest& request, std::vector& key); [[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span key); [[nodiscard]] const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle); +[[nodiscard]] std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle); [[nodiscard]] std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle); [[nodiscard]] std::shared_ptr ResolveSource(const RecompileRequest& request); [[nodiscard]] std::shared_ptr CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index f7d2ff5b2..2b7f9cf59 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1216,6 +1216,18 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) { return handle.artifact->artifact; } +std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) { + if (!request.target.registrationModules) throw std::runtime_error("ShaderRecompiler: registration results need registration modules"); + if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); + const auto& info = handle.artifact->info.info; + ResourceSnapshot snapshot; + snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); + snapshot.images.assign(info.images.size(), {{}, 8u}); + snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); + snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); + return std::make_shared(materializeResult(*handle.artifact, request, snapshot)); +} + std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) { if (!MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); if (capture.source != handle.source || capture.plan != handle.source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); From e9def903fdba1f9384ae3896ef531b20e2a89167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 16:10:39 +0200 Subject: [PATCH 07/19] perf(agc): prebuild each stage's library alone and keep two descriptor sets with pipeline libraries Co-Authored-By: Claude --- .../src/Driver/Shaders/ShaderRegistry.cpp | 46 ++++++++++++------- .../Execution/src/VulkanDevice.cpp | 23 +++++++++- .../Graphics/include/Pipeline.hpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 3 +- .../Graphics/src/ShaderResources.cpp | 19 +++++--- .../tests/execution/PreparedGraphics.cpp | 24 ++++++---- 6 files changed, 82 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index ed72dcd0d..03e3fdd3a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -573,6 +573,7 @@ struct RegisteredPreparation { std::array memory{}; std::optional request; std::vector abiKey; + std::shared_ptr prebuildDevice; }; std::unique_ptr PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) { @@ -671,6 +672,18 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap return plan; } +void PrebuildStageLibrary(const std::shared_ptr& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr result, std::uint32_t pushOffset) { + if (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment) return; + SubmitRegistrationPreparation([device, stages, stage, result = std::move(result), pushOffset] { + const std::array shaders{{{stage, result.get(), pushOffset}}}; + try { + static_cast(device->PrebuildGraphicsLibraries(stages, shaders)); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + }); +} + std::vector PrepareRegistered(RegisteredPreparation& plan) { auto& request = *plan.request; std::vector entries; @@ -680,6 +693,12 @@ std::vector PrepareRegistered(RegisteredPreparation& pla entries.push_back({plan.codeOffset, PrepareShaderWithDiagnostics(request, &preparation)}); }; append(); + if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) { + try { + PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes); + } catch (const std::exception&) { + } + } if (request.context.compute) { request.context.compute->partialThreads = {1, 1, 1}; append(); @@ -939,22 +958,10 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std if (stage.snapshot->type == 1) continue; ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); } - if (localDevice->Target().registrationModules && !prepared.empty() && std::ranges::all_of(prepared, [](const auto& stage) { return stage.registration != nullptr && (stage.stage == ShaderRecompiler::ShaderStage::Vertex || stage.stage == ShaderRecompiler::ShaderStage::Fragment); }) && prepared.back().stage == ShaderRecompiler::ShaderStage::Fragment) { - std::vector> results; - std::vector> stages; + if (decoded.state.stages.path == Graphics::ShaderPath::Vertex) { for (const auto& stage : prepared) { - results.push_back(stage.registration); - stages.emplace_back(stage.stage, stage.pushOffset); + if (stage.registration != nullptr) PrebuildStageLibrary(localDevice, decoded.state.stages, stage.stage, stage.registration, stage.pushOffset); } - SubmitRegistrationPreparation([localDevice, results = std::move(results), stages = std::move(stages), shaderStages = decoded.state.stages] { - std::vector shaders; - for (std::size_t index = 0; index < results.size(); ++index) shaders.push_back({stages[index].first, results[index].get(), stages[index].second}); - try { - static_cast(localDevice->PrebuildGraphicsLibraries(shaderStages, shaders)); - } catch (const std::exception& error) { - APS5_LOG_OUT("graphics library prebuild skipped: %s", error.what()); - } - }); } PreparedShaderState::GraphicsAbi abi{key, registry, {}}; for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot); @@ -996,14 +1003,20 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + const auto prepare = [&] { + const auto plan = PlanRegistered(*snapshot, *localDevice, state, false); + if (plan == nullptr) return std::vector{}; + plan->prebuildDevice = localDevice; + return PrepareRegistered(*plan); + }; const bool committedAbi = CommittedAbi(*snapshot, key); - auto entries = committedAbi ? std::vector{} : PrepareRegistered(*snapshot, *localDevice, state, false); + auto entries = committedAbi ? std::vector{} : prepare(); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { transaction.Commit(); return; } - if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false); + if (committedAbi) entries = prepare(); auto& prepared = transaction.Edit(*snapshot); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; }); @@ -1190,6 +1203,7 @@ void Driver::RegisterShader(const Shader* shader) { std::shared_ptr preparation; if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); + if (preparation != nullptr) preparation->prebuildDevice = localDevice; if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey); snapshot.prepared->pending = preparation != nullptr; } else { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 78d96fe75..d77cdb3b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3073,8 +3073,27 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage state.blend.colorWriteMask = 15; state.blends.push_back(state.blend); state.renderExtent = {1, 1}; - const auto layouts = Graphics::DescribeSetLayouts(context, shaders); - return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), shaders); + std::vector group(shaders.begin(), shaders.end()); + if (group.size() == 1u) { + const bool vertex = group.front().stage == ShaderRecompiler::ShaderStage::Fragment; + static const auto stub = [](bool vertexStub) { + auto result = std::make_shared(); + const std::uint32_t model = vertexStub ? 0u : 4u; + std::vector words{0x07230203u, 0x00010000u, 0u, 6u, 0u, 0x00020011u, 1u, 0x0003000eu, 0u, 1u, 0x0005000fu, model, 4u, 0x6e69616du, 0u}; + if (!vertexStub) words.insert(words.end(), {0x00030010u, 4u, 7u}); + words.insert(words.end(), {0x00020013u, 2u, 0x00030021u, 3u, 2u, 0x00050036u, 2u, 4u, 0u, 3u, 0x000200f8u, 5u, 0x000100fdu, 0x00010038u}); + result->spirv = std::move(words); + result->variantId = vertexStub ? ~std::uint64_t{0} - 1u : ~std::uint64_t{0} - 2u; + result->runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version; + return result; + }; + static const auto vertexStub = stub(true); + static const auto fragmentStub = stub(false); + if (vertex) group.insert(group.begin(), {ShaderRecompiler::ShaderStage::Vertex, vertexStub.get(), 0u}); + else group.push_back({ShaderRecompiler::ShaderStage::Fragment, fragmentStub.get(), Graphics::PipelinePushSlotBytes}); + } + const auto layouts = Graphics::DescribeSetLayouts(context, group); + return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group); } void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index f3a48a189..6e6ca7fdf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -108,6 +108,7 @@ struct DescribedSetLayouts { PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; } }; DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders); +bool SplitsSets(const Context& context, std::span shaders); // Destroys the cached pipelines of a device; to be called before the device goes away. Without it, // entries of a gone device are recognised by their buffer pool (made and reset with the device, so // it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 911f1a10a..af0b4e40b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -477,13 +477,14 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput _modules.clear(); return; } - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, prebuilding); if (prebuilding) { + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, true)); for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); prebuilt = true; return; } + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7bee0f6a0..2214d3c90 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1425,7 +1425,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions(); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - splitSets = std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }); + splitSets = SplitsSets(context, shaders); std::array, 2> descriptions; for (const auto& binding : bindings) { const auto set = setOf(binding); @@ -1449,7 +1449,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons _layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout); if (splitSets) { fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); - if (!descriptions[1].empty()) { + { const auto sizes = PoolSizes(descriptions[1]); if (context.descriptorCache != nullptr && !noPoolCache) { fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind); @@ -1465,7 +1465,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } } - if (splitSets && !descriptions[0].empty()) { + if (splitSets) { const auto sizes = PoolSizes(descriptions[0]); if (context.descriptorCache != nullptr && !noPoolCache) { cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); @@ -1513,11 +1513,17 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } +bool SplitsSets(const Context& context, std::span shaders) { + const auto fragmentSet = [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }; + const auto fragmentStage = [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment; }; + return std::any_of(shaders.begin(), shaders.end(), fragmentSet) || (context.graphicsPipelineLibrary && std::any_of(shaders.begin(), shaders.end(), fragmentStage)); +} + DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders) { Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache"); DescribedSetLayouts result; std::array, 2> descriptions; - bool split = false; + const bool split = SplitsSets(context, shaders); bool updateAfterBind = false; std::uint64_t storageBuffers = 0; for (const auto& shader : shaders) { @@ -1526,7 +1532,6 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::spanbindings) { - split |= binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER; if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count; @@ -3558,11 +3563,11 @@ std::shared_ptr ShaderResources::PrepareDrawBindi std::array, 2> descriptions; for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout); result->cache = context.descriptorCache; - if (!descriptions[0].empty()) { + if (!descriptions[0].empty() || splitSets) { result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0])); Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } - if (!descriptions[1].empty()) { + if (!descriptions[1].empty() || splitSets) { result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1])); Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 7b4970657..e405f2c16 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -132,20 +132,28 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path const auto stages = PrepareGraphicsStages(prepared, target); Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages"); if (!mesh && !tessellation && target.registrationModules) { - const auto prebuild = [&](const std::vector& group) { - Require(group.front().registration != nullptr && group.back().registration != nullptr, "registration modules: a prepared stage has no registration result"); - const std::array shaders{{{group.front().stage, group.front().registration.get(), group.front().pushOffset}, {group.back().stage, group.back().registration.get(), group.back().pushOffset}}}; + const auto prebuild = [&](const std::vector& group, std::initializer_list picked) { + std::vector shaders; + for (const auto index : picked) { + Require(group.at(index).registration != nullptr, "registration modules: a prepared stage has no registration result"); + shaders.push_back({group[index].stage, group[index].registration.get(), group[index].pushOffset}); + } const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt"); const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); - return std::pair{after.built[1] - before.built[1] + after.built[2] - before.built[2], after.linked - before.linked}; + Require(after.linked == before.linked, "registration modules: a prebuild linked a pipeline"); + return std::pair{after.built[1] - before.built[1], after.built[2] - before.built[2]}; }; - const auto [first, firstLinks] = prebuild(stages); - Require(first == 2u && firstLinks == 0u, "registration modules: the prebuild did not build exactly the two shader libraries without linking"); + const auto vertexOnly = prebuild(stages, {0u}); + Require(vertexOnly.first == 1u, "registration modules: the vertex stage alone did not build its pre-rasterization library"); + const auto pixelOnly = prebuild(stages, {1u}); + Require(pixelOnly.second == 1u, "registration modules: the pixel stage alone did not build its fragment library"); + const auto pair = prebuild(stages, {0u, 1u}); + Require(pair.first == 0u && pair.second == 0u, "registration modules: the pair of stages did not find the libraries prebuilt one stage at a time"); const auto again = PrepareGraphicsStages(prepared, target); Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object"); - const auto [second, secondLinks] = prebuild(again); - Require(second == 0u && secondLinks == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); + const auto repeated = prebuild(again, {0u, 1u}); + Require(repeated.first == 0u && repeated.second == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); } for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle); From 67556722e1bd63408cdab8b6a15eb85f2f5c83da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 16:23:59 +0200 Subject: [PATCH 08/19] perf(recompiler): read vertex attribute component selectors at run time when modules are built at registration Co-Authored-By: Claude --- .../tests/execution/ZeroStrideVertexFetch.cpp | 17 ++++++++++--- .../Optimization/src/BindingAllocator.cpp | 9 +++++++ core/shader/recompiler/Recompiler.cpp | 25 ++++++++++++++++--- core/shader/recompiler/RuntimeAbi.hpp | 2 ++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index 50e546f07..a308a4063 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -37,11 +37,14 @@ struct Case { std::uint32_t records; bool inRange; const char* what; + std::uint32_t selectors = 0xfacu; }; +std::vector registrationModule; + std::array VertexBufferDescriptor(const Case& value) { const auto address = reinterpret_cast(Element.data()); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, 0x0104dfacu | (value.select << 28u)}; + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, (0x0104dfacu & ~0xfffu) | value.selectors | (value.select << 28u)}; } std::array OutputDescriptor() { @@ -74,6 +77,10 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { vertexRequest.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest); Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute"); + if (device.Target().registrationModules) { + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Words(); + Require(vertexResult.spirv.Words() == registrationModule, std::string(value.what) + ": registration modules: the V# specialized the vertex module"); + } const auto vertexPush = static_cast(vertexResult.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixel{}; @@ -120,7 +127,9 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { void Check(const Case& value) { for (std::uint32_t vertex = 0; vertex < Vertices; ++vertex) { for (std::uint32_t component = 0; component < 4u; ++component) { - const auto expected = value.inRange ? std::bit_cast(Element[component]) : 0u; + const auto selector = (value.selectors >> (component * 3u)) & 7u; + const auto selected = selector >= 4u ? std::bit_cast(Element[selector - 4u]) : selector == 1u ? std::bit_cast(1.0f) : 0u; + const auto expected = value.inRange || selector < 4u ? selected : 0u; const auto actual = Output[vertex * 4u + component]; Require(actual == expected, std::string(value.what) + ": vertex " + std::to_string(vertex) + " v" + std::to_string(8u + component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); } @@ -133,12 +142,14 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - constexpr std::array cases{{ + constexpr std::array cases{{ {2u, 1u, true, "OOB_SELECT 2, one record"}, {2u, 0u, false, "OOB_SELECT 2, no records"}, {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, {3u, 0u, false, "OOB_SELECT 3, no records"}, {0u, 16u, true, "OOB_SELECT 0, whole element in range"}, + {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, + {3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, }}; for (const auto& value : cases) { Draw(*device, value); diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 3f58c1398..ceaca33ff 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -104,6 +104,15 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords; + if (registrationModules && program.Resources().stage != IrShaderStage::Pixel) { + std::uint32_t attributes = 0u; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetAttribute && inst->Flags() == 1u) attributes = std::max(attributes, inst->Argument(0)->Resolve()->ImmediateU32() + 1u); + } + } + next.runtimeStateDwords = (attributes + RuntimeAbi::VertexSelectorsPerDword - 1u) / RuntimeAbi::VertexSelectorsPerDword; + } const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2b7f9cf59..e09f87430 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1,5 +1,6 @@ #include "Recompiler.hpp" #include +#include #include #include #include @@ -714,7 +715,7 @@ std::shared_ptr specializeModule(const CompiledShaderAr return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot) { +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -751,6 +752,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; + std::array runtimeSelectors{}; for (const auto& input : result.vertexInputs) { if (!request.context.vertex || input.location >= request.context.vertex->resourcesNum || input.location >= request.context.vertex->resources.size()) throw std::runtime_error("Shader cache: invalid vertex attribute metadata"); const auto& vertex = *request.context.vertex; @@ -758,7 +760,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto fetchIndex = vertex.fetchEmbedded ? vertex.resourcesDst[input.location].fetchIndex : input.fetchIndex; result.vertexAttributes.push_back({input.location, input.components, resource, fetchIndex}); if (!artifact.vertexInputPatches.empty()) { - const auto numeric = VertexInputNumericClass(static_cast((resource.fields[3] >> 12u) & 0x7fu)); + const auto format = static_cast((resource.fields[3] >> 12u) & 0x7fu); + const auto numeric = registrationDefaults && format == IrBufferFormat::Invalid ? IrTextureNumericClass::Float : VertexInputNumericClass(format); if (numeric == IrTextureNumericClass::Unsupported) throw std::runtime_error("unsupported prepared vertex format"); const auto kind = numeric == IrTextureNumericClass::Float ? 0u : numeric == IrTextureNumericClass::Sint ? 1u : 2u; vertexClasses.at(input.location) = kind; @@ -771,7 +774,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u); selectors |= selector << (component * 3u); } - moduleKey.push_back(selectors); + moduleKey.push_back(request.target.registrationModules ? selectors & ~0xfffu : selectors); + if (request.target.registrationModules) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { @@ -828,6 +832,19 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); + const auto& layout = variant.bindings.layout; + if (request.target.registrationModules && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { + const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; + if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity"); + if (layout.UsesPushData()) { + if ((first + layout.runtimeStateDwords) * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("vertex selector state lies outside the push data"); + std::memcpy(result.pushConstants.data() + first * sizeof(std::uint32_t), runtimeSelectors.data(), layout.runtimeStateDwords * sizeof(std::uint32_t)); + } else { + const auto data = std::ranges::find_if(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::ShaderData; }); + if (data == result.bindings.end() || first + layout.runtimeStateDwords > data->guestDescriptor.size()) throw std::runtime_error("vertex selector state lies outside the shader data"); + std::copy_n(runtimeSelectors.begin(), layout.runtimeStateDwords, data->guestDescriptor.begin() + first); + } + } result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; return result; } @@ -1225,7 +1242,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest snapshot.images.assign(info.images.size(), {{}, 8u}); snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); - return std::make_shared(materializeResult(*handle.artifact, request, snapshot)); + return std::make_shared(materializeResult(*handle.artifact, request, snapshot, true)); } std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) { diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 2a303c504..eb5ce8cfc 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -42,6 +42,8 @@ inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; inline constexpr std::uint32_t StateExportMappings = 0u; inline constexpr std::uint32_t StateDwords = 2u; +inline constexpr std::uint32_t StateVertexSelectors = 0u; +inline constexpr std::uint32_t VertexSelectorsPerDword = 2u; inline constexpr std::uint32_t ImageModeMask = 0xffu; inline constexpr std::uint32_t CompareStateShift = 9u; inline constexpr std::uint32_t CompareStateMask = 0x1fffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index ac2cc1366..f20d094a8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1250,7 +1250,7 @@ std::uint32_t EmitGetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) const auto component = inst.Argument(1)->ImmediateU32(); if (state.inputInfo.vertex == nullptr || !state.inputInfo.vertex->fetchEmbedded || component >= 4u) ctx.Fail(inst, "invalid prepared vertex attribute"); const auto first = PipelineSpecialization::VertexBase + attribute * PipelineSpecialization::VertexWords; - const auto selector = state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); + const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors + attribute / RuntimeAbi::VertexSelectorsPerDword), ConstantU32(state, attribute % RuntimeAbi::VertexSelectorsPerDword * 16u + component * 3u)), 7u) : state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); const auto one = state.module.SpecializationConstant(TypeU32(state), first + 4u, 0x3f800000u); auto value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, 1u)), one, ConstantU32(state, 0u)); for (std::uint32_t channel = 0; channel < 4u; ++channel) value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, channel + 4u)), EmitAttributeValue(state, attribute, channel), value); From 9eb42ca2d24523b424fbbd9ec304f0d280575754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 00:02:12 +0200 Subject: [PATCH 09/19] fix(agc): destroy pipeline library shader modules once the library is built Co-Authored-By: Claude --- .../Graphics/include/PipelineLibrary.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 ++-- .../Graphics/src/PipelineLibrary.cpp | 15 +-------------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index 5541197b2..d121692b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -33,7 +33,7 @@ struct OptimizedPipeline { void Release() noexcept; }; -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}, bool partsOnly = false); +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false); void WaitForOptimizedPipelines(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index af0b4e40b..31cd07f34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -478,13 +478,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput return; } if (prebuilding) { - static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, true)); + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true)); for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); prebuilt = true; return; } - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized); else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index a6bbdb1cb..7f8ebfca3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -38,10 +38,8 @@ using Key = std::vector; struct DeviceLibraries { PFN_vkDestroyPipeline destroyPipeline = nullptr; PFN_vkDestroyPipelineLayout destroyLayout = nullptr; - PFN_vkDestroyShaderModule destroyModule = nullptr; std::map layouts; std::array, 4> parts; - std::vector modules; PipelineLibraryCounters counters; }; @@ -102,7 +100,7 @@ std::span PipelineLibraryDynamicStates() { return dynamicStates; } -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules, bool partsOnly) { +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); auto& store = Libraries(); std::unique_lock lock(store.mutex); @@ -110,7 +108,6 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); device.destroyLayout = context.Function("vkDestroyPipelineLayout"); - device.destroyModule = context.Function("vkDestroyShaderModule"); } const auto layoutFor = [&](const Key& key, std::optional set) { auto found = device.layouts.find(key); @@ -203,15 +200,6 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip continue; } cache.emplace(key, handle); - if (optimize) { - for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { - for (auto& module : modules) { - if (module == VK_NULL_HANDLE || module != create.pStages[stage].module) continue; - device.modules.push_back(module); - module = VK_NULL_HANDLE; - } - } - } ++device.counters.built[part]; libraries.push_back(handle); } @@ -275,7 +263,6 @@ void ClearPipelineLibraries(VkDevice device) { for (auto& part : libraries.parts) { for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr); } - for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr); for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr); store.devices.erase(found); } From a326fa84f7595afc9cf63480bbdf18b58c175ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 00:02:12 +0200 Subject: [PATCH 10/19] fix(recompiler): give registration results a flattened SRT placeholder Co-Authored-By: Claude --- core/shader/recompiler/Recompiler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index e09f87430..df88fc11a 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1242,6 +1242,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest snapshot.images.assign(info.images.size(), {{}, 8u}); snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); + snapshot.flattenedSrt.assign(1u, 0u); return std::make_shared(materializeResult(*handle.artifact, request, snapshot, true)); } From 361c7e2024baa92d1b93265add7c692730c10b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 01:39:28 +0200 Subject: [PATCH 11/19] perf(agc): keep oversized shaders specialized and prebuild libraries on their own thread Co-Authored-By: Claude --- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 28 +++++++++++++----- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++- .../tests/execution/VertexFetch.cpp | 5 +++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 2 ++ .../Optimization/DescriptorBindingBuilder.hpp | 2 +- .../src/DescriptorBindingBuilder.cpp | 4 +-- core/shader/recompiler/Recompiler.cpp | 29 +++++++++++++------ core/shader/recompiler/Recompiler.hpp | 2 ++ core/shader/recompiler/ShaderDiskCache.cpp | 6 ++-- .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- 16 files changed, 67 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index f0ee74af4..ad90c2db7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -103,6 +103,7 @@ std::shared_ptr RegisteredHeader(const ShaderRegistry& reg std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types); void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task); +void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 03e3fdd3a..502cf603c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -673,8 +673,8 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } void PrebuildStageLibrary(const std::shared_ptr& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr result, std::uint32_t pushOffset) { - if (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment) return; - SubmitRegistrationPreparation([device, stages, stage, result = std::move(result), pushOffset] { + if (result == nullptr || (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment)) return; + SubmitLibraryPrebuild([device, stages, stage, result = std::move(result), pushOffset] { const std::array shaders{{{stage, result.get(), pushOffset}}}; try { static_cast(device->PrebuildGraphicsLibraries(stages, shaders)); @@ -696,7 +696,8 @@ std::vector PrepareRegistered(RegisteredPreparation& pla if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) { try { PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes); - } catch (const std::exception&) { + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); } } if (request.context.compute) { @@ -717,7 +718,15 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { - static auto* pool = new RegistrationPreparePool(); + static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); + return *pool; + } + + static RegistrationPreparePool& Prebuild() { + static auto* pool = new RegistrationPreparePool([] { + const char* text = std::getenv("APS5_PREBUILD_THREADS"); + return text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u; + }()); return *pool; } @@ -741,8 +750,7 @@ public: } private: - RegistrationPreparePool() { - const auto count = std::max(2u, std::thread::hardware_concurrency() / 2u); + explicit RegistrationPreparePool(unsigned count) { pthread_attr_t attributes; pthread_attr_init(&attributes); pthread_attr_setstacksize(&attributes, 64u << 20u); @@ -823,8 +831,13 @@ void SubmitRegistrationPreparation(std::function task) { RegistrationPreparePool::Instance().Submit(std::move(task)); } +void SubmitLibraryPrebuild(std::function task) { + RegistrationPreparePool::Prebuild().Submit(std::move(task)); +} + void CloseRegistrationPreparation() { RegistrationPreparePool::Instance().Close(); + RegistrationPreparePool::Prebuild().Close(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { @@ -885,7 +898,8 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod if (target.registrationModules) { try { registration = ShaderRecompiler::RegistrationResult(request, *handle); - } catch (const std::exception&) { + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); } } prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)}); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index b0822a499..063d4a499 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -71,6 +71,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs"); require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ"); require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ"); + require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ"); require(left.vertexOffsetSgpr == right.vertexOffsetSgpr && left.instanceOffsetSgpr == right.instanceOffsetSgpr, prefix + "offset SGPRs differ"); require(left.vertexOffsetShared == right.vertexOffsetShared && left.instanceOffsetShared == right.instanceOffsetShared && left.vertexOffsetConflict == right.vertexOffsetConflict && left.instanceOffsetConflict == right.instanceOffsetConflict, prefix + "offset flags differ"); require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ"); @@ -153,6 +154,7 @@ RecompileResult sampleResult() { result.workgroupMemoryDwords = 32769u; result.vertexInputs = {{1, 4, 2}, {5, 2, 0}}; result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}}; + result.registrationModule = true; result.specialization = {{512, 4}, {516, 0x3f800000u}, {517, 0}}; result.vertexAttributes = {{1, 4, {{0x1000u, 0x20000u, 0x30u, 0x4u}}, 2}, {5, 2, {{9u, 8u, 7u, 6u}}, 0}}; result.vertexInputs[0].outputMask = 5u; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e191f6d1f..03553bad8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3220,7 +3220,9 @@ int main(int argc, char** argv) { require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization"); auto registrationModules = uncached; registrationModules.target.registrationModules = true; - require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target.registrationModules, "registration modules were lost in serialization"); + registrationModules.target.registrationWordLimit = 1234u; + const auto roundTrip = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target; + require(roundTrip.registrationModules && roundTrip.registrationWordLimit == 1234u, "registration modules were lost in serialization"); std::vector plainKey; std::vector registrationKey; RecompileCacheKey::Build(uncached, plainKey); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp index c42f5c35c..14df999eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp @@ -53,8 +53,9 @@ void Run(AgcDriver::VulkanDevice& device) { std::uint32_t format; bool moved; std::uint32_t offset; + bool specializedPixel = false; }; - constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}}}; + constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}, {16u, 77u, false, 1u, true}}}; for (const auto& value : cases) { auto& storage = value.moved ? movedVertices : vertices; storage.fill(std::byte{0}); @@ -86,7 +87,9 @@ void Run(AgcDriver::VulkanDevice& device) { pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + if (value.specializedPixel) pixelRequest.target.registrationWordLimit = 1u; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + if (device.Target().registrationModules) Require(vertex.registrationModule && pixel.registrationModule != value.specializedPixel, "registration modules: the word limit did not choose the module mode"); const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index b50278685..944d076de 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -241,6 +241,7 @@ private: append(key, value.narrowSubgroupClock); append(key, value.float64DenormPreserve); append(key, value.registrationModules); + append(key, value.registrationWordLimit); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index b2f5d2007..2bab2bb46 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -755,6 +755,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.narrowSubgroupClock); writer.WriteBool(request.target.float64DenormPreserve); writer.WriteBool(request.target.registrationModules); + writer.WriteU32(request.target.registrationWordLimit); return base64Encode(buffer); } @@ -785,6 +786,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool(); if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool(); if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); return result; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 6c15439dc..84341598a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -35,7 +35,7 @@ struct DescriptorBindingPlan { class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false) const; + DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index b76a88dce..5277129c9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -252,7 +252,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con allocation.specialization = std::move(plan.specialization); } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules, bool fragmentDescriptorSet) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; @@ -379,7 +379,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; - physical.descriptorSet = registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; + physical.descriptorSet = fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; physical.binding = NativeBinding(stage, compact.kind); physical.count = compact.resources.empty() ? 1u : static_cast(compact.resources.size()); physical.kind = PhysicalKindFor(compact.kind); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index df88fc11a..727334c6e 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -408,7 +408,8 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); + bool registration = request.target.registrationModules; + auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -420,12 +421,21 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; - targetOptions.registrationModules = request.target.registrationModules; + targetOptions.registrationModules = registration; + targetOptions.fragmentDescriptorSet = request.target.registrationModules; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; result.variantId = nextVariantId(); result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + if (registration && result.spirv.size() > request.target.registrationWordLimit) { + registration = false; + program.Metadata().bindingLayoutComplete = false; + bindings = bindingAllocator.Allocate(program, request.layout, false); + targetOptions.registrationModules = false; + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + } + result.registrationModule = registration; result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -636,7 +646,7 @@ std::shared_ptr registrationModule(std::uint64_t varian return entry->module; } -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool registrationModules) { +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -677,7 +687,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, registrationModules); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet); } catch (...) { plan->failure = std::current_exception(); } @@ -724,6 +734,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .imageMetadataDword = artifact.imageMetadataDword, .runtimeImageCount = artifact.runtimeImageCount, .runtimeImageResources = artifact.runtimeImageResources, + .registrationModule = artifact.registrationModule, .bdaAbiVersion = artifact.bdaAbiVersion, .runtimeAbiVersion = artifact.runtimeAbiVersion, .vertexInputs = artifact.vertexInputs, @@ -774,8 +785,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u); selectors |= selector << (component * 3u); } - moduleKey.push_back(request.target.registrationModules ? selectors & ~0xfffu : selectors); - if (request.target.registrationModules) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); + moduleKey.push_back(artifact.registrationModule ? selectors & ~0xfffu : selectors); + if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { @@ -806,7 +817,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = request.target.registrationModules && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); + const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); @@ -833,7 +844,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); const auto& layout = variant.bindings.layout; - if (request.target.registrationModules && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { + if (artifact.registrationModule && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity"); if (layout.UsesPushData()) { @@ -1234,8 +1245,8 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) { } std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) { - if (!request.target.registrationModules) throw std::runtime_error("ShaderRecompiler: registration results need registration modules"); if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); + if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr; const auto& info = handle.artifact->info.info; ResourceSnapshot snapshot; snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 731ffa1c7..5a0da6a5e 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -230,6 +230,7 @@ struct SpirvTarget { bool fixedPushSlots = false; bool float64DenormPreserve = false; bool registrationModules = false; + std::uint32_t registrationWordLimit = 200000u; }; struct BindingLayout { @@ -476,6 +477,7 @@ struct CompiledShaderArtifact { std::uint32_t imageMetadataDword = 0; std::uint32_t runtimeImageCount = 0; std::vector runtimeImageResources; + bool registrationModule = false; std::uint32_t bdaAbiVersion = 0; std::uint32_t runtimeAbiVersion = RuntimeAbi::Version; std::vector vertexInputs; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 0949aef04..24b6a9e37 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,9 +64,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 192, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 200, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 312, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 320, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 568, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -297,6 +297,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.imageMetadataDword); writer.Value(result.runtimeImageCount); writer.Values(std::span(result.runtimeImageResources)); + writer.Value(result.registrationModule); writer.List(result.vertexInputPatches, [](Writer& out, const VertexInputPatch& patch) { out.Value(patch.location); out.Value(patch.word); @@ -339,6 +340,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.imageMetadataDword); reader.Value(result.runtimeImageCount); reader.Values(result.runtimeImageResources); + reader.Value(result.registrationModule); reader.List(result.vertexInputPatches, 20, [](Reader& in, VertexInputPatch& patch) { in.Value(patch.location); in.Value(patch.word); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index dbd24615a..65cb02117 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -22,6 +22,7 @@ struct SpirvTargetOptions { bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool registrationModules = false; + bool fragmentDescriptorSet = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index ff712c17e..670bd401e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -92,6 +92,7 @@ struct SpirvEmitterState { bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool registrationModules = false; + bool fragmentDescriptorSet = false; std::uint32_t bufferMisalignment = 0; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index df64b366a..a3be534ad 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -251,6 +251,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; state.registrationModules = target.registrationModules; + state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 4227e8d83..58fce16aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -391,7 +391,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) { const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage); state.module.AddName(variable, name); - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind)); return variable; }; From acc0a0487d955ee5b58674b2684a937798a640e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 01:50:10 +0200 Subject: [PATCH 12/19] docs: describe shader modules and pipeline libraries built at registration Co-Authored-By: Claude --- docs/dev/ARCHITECTURE.md | 1 + docs/dev/TechnicalDebt.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 203602f2b..4d5121846 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -91,6 +91,7 @@ flowchart LR - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4481d2cbc..b5759c172 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -405,6 +405,8 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. +- [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. From d809e6d0db403947064a5a651429f7e67a4adfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 02:55:45 +0200 Subject: [PATCH 13/19] fix(agc): close the library prebuild pool only if it was created Co-Authored-By: Claude --- .../src/Driver/Shaders/ShaderRegistry.cpp | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 502cf603c..ad9c4f949 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -723,13 +723,27 @@ public: } static RegistrationPreparePool& Prebuild() { - static auto* pool = new RegistrationPreparePool([] { + static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); - return text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u; - }()); + auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); + PrebuildSlot().store(created); + return created; + }(); return *pool; } + static RegistrationPreparePool* ExistingPrebuild() { + return PrebuildSlot().load(); + } + +private: + static std::atomic& PrebuildSlot() { + static std::atomic slot{nullptr}; + return slot; + } + +public: + void Submit(std::function task) { { std::unique_lock lock(mutex); @@ -837,7 +851,7 @@ void SubmitLibraryPrebuild(std::function task) { void CloseRegistrationPreparation() { RegistrationPreparePool::Instance().Close(); - RegistrationPreparePool::Prebuild().Close(); + if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { From 672e43ca678ff8d50890b5f5154fe5095579994b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 03:06:20 +0200 Subject: [PATCH 14/19] perf(recompiler): keep compute shaders specialized; only prebuilt vertex and pixel shaders take the registration form Co-Authored-By: Claude --- .../tests/execution/BufferDescriptorRuntime.cpp | 8 +------- .../tests/execution/EmulatedColorCompare.cpp | 2 -- .../prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp | 3 +-- core/shader/recompiler/Recompiler.cpp | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index eafac530e..45364ef78 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -72,8 +72,6 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { {"records 64 base +2", 2, 64, true}, {"absent", 0, 128, false}, }}; - const void* first = nullptr; - std::uint64_t variant = 0; for (const auto& test : cases) { Output.fill(Fill); const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; @@ -84,11 +82,7 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { runtime.userData = userData; const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); - if (first == nullptr) { - first = result->spirv.data(); - variant = result->PipelineVariantId(); - } - if (registrationModules) Require(result->spirv.data() == first && result->PipelineVariantId() == variant, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); + Require(!result->registrationModule, "buffer descriptor runtime, " + mode + ", " + test.name + ": a compute shader was emitted in registration form"); device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); for (std::uint32_t lane = 0; lane < Lanes; ++lane) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 3347835d2..cc3452070 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -243,7 +243,6 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { - if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); } @@ -294,7 +293,6 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char Output.fill(-1.0f); const auto result = Compile(device, Format8888UNorm, sampler, CubeCode, false, true, true); for (const auto& binding : result.bindings) { - if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "cube emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "cube emulated comparison retained an unused sampler association"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp index ba1c7bb84..77b95c736 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp @@ -30,7 +30,7 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: const auto instructions = store ? std::span(storeCode) : std::span(code); ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(instructions.data()), instructions, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u}}; auto result = ShaderRecompiler::Recompile(request); - Require(result.runtimeImageCount == (request.target.registrationModules ? 1u : 0u), "direct image runtime metadata does not follow the module mode"); + Require(result.runtimeImageCount == 0u, "direct image allocated runtime image metadata"); Require(result.shaderDataDwords < ShaderRecompiler::RuntimeAbi::ShaderDataDwords, "shader data was not compacted"); for (const auto& binding : result.bindings) { if (binding.role == ShaderRecompiler::DescriptorRole::GuestImages) Require(binding.count == 1u, "single image expanded into unused descriptor slots"); @@ -48,7 +48,6 @@ void Run(AgcDriver::VulkanDevice& device) { device.Dispatch(shaders.back(), 1u, 1u, 1u); device.SubmitRecorded(); } - if (device.Target().registrationModules) Require(shaders[0].spirv == shaders[1].spirv && shaders[0].spirv == shaders[2].spirv, "registration modules: a present or a null image specialized the module"); shaders.clear(); device.WaitIdle(); for (std::uint32_t batch = 0u; batch < outputs.size(); ++batch) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 727334c6e..770b10811 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -408,7 +408,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - bool registration = request.target.registrationModules; + bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; From 68164aee2a6d27f648aeef1c144cda07ec7596bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 03:18:05 +0200 Subject: [PATCH 15/19] docs: note that compute shaders keep the specialized form Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b5759c172..1e2429348 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -405,7 +405,7 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline -- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy From 60151b66b7e7db08d9106552e570253015d250d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 03:53:11 +0200 Subject: [PATCH 16/19] perf(recompiler): keep the format word specialized for buffers accessed with the descriptor's format Co-Authored-By: Claude --- .../recompiler/Optimization/src/DescriptorBindingBuilder.cpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp | 3 ++- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 5277129c9..f88bd777d 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -282,6 +282,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); + if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]}); if (!registrationModules) { const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 770b10811..2a214225d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1248,6 +1248,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr; const auto& info = handle.artifact->info.info; + if (std::ranges::any_of(info.buffers, [](const BufferResource& buffer) { return buffer.descriptorFormatted; }) || std::ranges::any_of(info.samplers, [](const SamplerResource& sampler) { return FoldsTexelOffsets(sampler); })) return nullptr; ResourceSnapshot snapshot; snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); snapshot.images.assign(info.images.size(), {{}, 8u}); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 0a3a991da..d31f80c45 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -60,7 +60,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); - const auto word3 = runtime ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted); + const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1e2429348..a5b631e2a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -405,7 +405,7 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline -- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy From 56d5a4985dda3e7dfda3b18209f72f841a106afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 09:14:43 +0200 Subject: [PATCH 17/19] perf(recompiler): read the formatted buffer helper's V# words as specialization constants when the format is known Co-Authored-By: Claude --- .../tests/FormattedBufferOnce.cpp | 17 ++++++++++++----- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index d4ae978ef..a05fce1a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -55,24 +55,27 @@ Counts Count(std::span words) { return counts; } -Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false) { +Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false) { std::vector code; if (gpu) code.push_back(0x7e160500u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0000000u | (load * 4u), 0x80020000u | ((10u + load) << 8u)}); code.push_back(0xbf8c3f70u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {(formattedStores ? 0xe0100000u : 0xe0700000u) | (load * 4u), 0x80010000u | ((10u + load) << 8u)}); + if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u}); code.push_back(0xbf810000u); const std::span words(code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; request.target = Target(); + request.target.registrationModules = registration; request.layout = {0, 0, 0, 128}; request.context.waveSize = 32; request.context.userDataBaseRegister = 0; request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + if (vertex) request.context.vertex = ShaderVertexStageInfo{}; + else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; request.context.memory = memory; request.useCache = false; return Count(Recompile(request).spirv.Words()); @@ -85,6 +88,11 @@ int main() { const auto none = Compile(0); const auto one = Compile(1); const auto three = Compile(3); + const auto gpuOne = Compile(1, false, true); + const auto vertexSpecialized = Compile(1, false, false, true, false); + const auto vertexRegistration = Compile(1, false, false, true, true); + Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)"); + Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")"); Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function"); Check(three.calls == one.calls + 2 && three.functions == one.functions, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.calls - one.calls) + " calls and " + std::to_string(three.functions - one.functions) + " functions more, expected two calls and no function"); Check(three.switches == one.switches, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.switches - one.switches) + " more format switches"); @@ -94,12 +102,11 @@ int main() { Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); Check(three.words - one.words < one.words / 10u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words to a " + std::to_string(one.words) + "-word module"); - const auto gpuOne = Compile(1, false, true); const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); Check(gpuThree.words - gpuOne.words < gpuOne.words / 10u, "formatted buffer once: 2 more formatted loads from a GPU-selected descriptor added " + std::to_string(gpuThree.words - gpuOne.words) + " SPIR-V words to a " + std::to_string(gpuOne.words) + "-word module"); - std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu)\n", one.words, three.words, gpuOne.words, gpuThree.words); + std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu; vertex %zu specialized, %zu registration)\n", one.words, three.words, gpuOne.words, gpuThree.words, vertexSpecialized.words, vertexRegistration.words); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index d31f80c45..6c61c68df 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -431,8 +431,19 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; - state.bufferMisalignment = parameters[9]; - const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], parameters[5], parameters[6], nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; + RuntimeDescriptor shared{}; + if (state.registrationModules) { + state.bufferMisalignment = parameters[9]; + const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6]; + shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + } else { + const auto word1 = state.module.SpecializationConstant(u32, first, 0u); + const auto stride = field(state, word1, 16u, 14u); + const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); + const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; + } const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); From f9b592806c54883f79a4fc962113392aef6136b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 09:18:46 +0200 Subject: [PATCH 18/19] perf(recompiler): keep the buffer base misalignment specialized in registration form Co-Authored-By: Claude --- .../tests/FormattedBufferOnce.cpp | 3 +++ .../src/DescriptorBindingBuilder.cpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 - .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 19 +++++++------------ 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index a05fce1a4..55fe8e55d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -35,6 +35,7 @@ struct Counts { std::size_t calls = 0; std::size_t functions = 0; std::size_t notInlined = 0; + std::size_t compareExchanges = 0; }; Counts Count(std::span words) { @@ -46,6 +47,7 @@ Counts Count(std::span words) { const auto opcode = words[offset] & spv::OpCodeMask; if (opcode == spv::OpSwitch) ++counts.switches; if (opcode == spv::OpFunctionCall) ++counts.calls; + if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; if (opcode == spv::OpFunction) { ++counts.functions; if (count > 3 && (words[offset + 3] & spv::FunctionControlDontInlineMask) != 0) ++counts.notInlined; @@ -91,6 +93,7 @@ int main() { const auto gpuOne = Compile(1, false, true); const auto vertexSpecialized = Compile(1, false, false, true, false); const auto vertexRegistration = Compile(1, false, false, true, true); + Check(vertexRegistration.compareExchanges == vertexSpecialized.compareExchanges, "formatted buffer once: in registration form a store through an aligned V# kept the unaligned compare-exchange path (" + std::to_string(vertexRegistration.compareExchanges) + " compare-exchanges, " + std::to_string(vertexSpecialized.compareExchanges) + " specialized)"); Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)"); Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")"); Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function"); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index f88bd777d..5951e7877 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -283,6 +283,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]}); + if (registrationModules) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 3u, misalignment}); if (!registrationModules) { const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 670bd401e..cf99dfc6c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -93,7 +93,6 @@ struct SpirvEmitterState { bool float64DenormPreserve = false; bool registrationModules = false; bool fragmentDescriptorSet = false; - std::uint32_t bufferMisalignment = 0; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 6c61c68df..2218a608a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -144,8 +144,6 @@ std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& in const auto& memory = context.Memory(instruction); if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u); auto& state = context.state; - if (state.bufferMisalignment != 0u) return state.bufferMisalignment; - if (state.registrationModules) return Binary(state, spv::OpBitwiseAnd, TypeU32(state), context.Arg(*instruction.Argument(0)->Resolve(), 0u), ConstantU32(state, 3u)); return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u); } @@ -406,7 +404,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr auto function = state.formattedBufferFunctions.find(key); if (function == state.formattedBufferFunctions.end()) { const auto id = state.module.AllocateId(); - const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32); const auto label = state.currentLabel; const auto byteOffset = state.memoryByteOffsets.at(element); struct Restore { @@ -417,24 +415,22 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr ~Restore() { state.module.EndHelperFunction(); state.memoryByteOffsets.at(element) = byteOffset; - state.bufferMisalignment = 0u; state.currentLabel = label; } }; state.module.BeginHelperFunction(); const Restore restore{state, element, label, byteOffset}; state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); - std::array parameters{}; - for (std::uint32_t index = 0; index < (store ? 11u : 10u); ++index) { + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 10u : 9u); ++index) { parameters[index] = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionParameter, index == 10u ? valueType : u32, parameters[index]); + state.module.AddFunction(spv::OpFunctionParameter, index == 9u ? valueType : u32, parameters[index]); } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; RuntimeDescriptor shared{}; if (state.registrationModules) { - state.bufferMisalignment = parameters[9]; const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6]; shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; } else { @@ -444,7 +440,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; } - const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); state.module.AddFunction(spv::OpFunctionEnd); @@ -455,9 +451,8 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); }; const auto swizzle = flag(resource.swizzle); const auto valid = flag(resource.valid); - const auto misalignment = baseMisalignment(context, instruction); - if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment, data); - else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment); + if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, data); + else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid); return store ? ConstantU32(state, 0u) : result; } From b1ee794a663d567f660971fa5e3ceb65b90960e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 09:59:23 +0200 Subject: [PATCH 19/19] test(agc): give the formatted buffer vertex case clip distances and the zero-position pixel shader its push slot Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp | 4 ++-- .../libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index 55fe8e55d..24ed571cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -19,8 +19,8 @@ void Check(bool condition, const std::string& message) { } SpirvTarget Target() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp index b5c7ad5c5..db8010acb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp @@ -53,9 +53,9 @@ Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t pixelInfo.wave32 = true; pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); - ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u};