From e1d4e699bed60200cb704a628987b9bc56199e80 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:38:46 +0200 Subject: [PATCH] feat(agc): implement primitive-ordered pixel shaders with fragment shader interlock DecodeState and the draw precheck rejected DB_SHADER_CONTROL bit 16, PRIMITIVE_ORDERED_PIXEL_SHADER (POPS), which runs the ordered section of overlapping pixels' shaders in primitive order. The bit now reaches the recompiler as a pixel stage field, and such a pixel shader declares PixelInterlockOrderedEXT and runs its whole body between OpBeginInvocationInterlockEXT and OpEndInvocationInterlockEXT; an invocation that a BDA read fault kills ends the interlock first. The POPS collision SGPRs read 0, so a title skips its POPS wait and runs its per-collision loop once. An image_load still reads a sampled binding, which misses an earlier overlapping primitive's image_store in the same draw. The device enables VK_EXT_fragment_shader_interlock when it has fragmentShaderPixelInterlock; without it the recompile throws, as does a nonzero POPS_OVERLAP_NUM_SAMPLES. The field is part of the recompile cache key, the draw key and the serialized request (version 10). --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 18 ++ .../Graphics/src/ShaderInputState.cpp | 1 + .../Graphics/src/ShaderValidation.cpp | 11 +- .../libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 137 ++++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 24 +- .../tests/execution/PixelInterlock.cpp | 239 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/src/SpirvBdaFault.cpp | 3 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 3 + .../src/SpirvMemory/SpirvInputOutput.cpp | 4 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 8 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 6 + .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/TechnicalDebt.md | 3 + 21 files changed, 462 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index cdc931174..8d42a6ae6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -63,6 +63,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); + mix(pixel.orderedPixelShader); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); std::lock_guard cacheLock(drawCacheMutex); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 581bab1a0..2f83ed263 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -98,7 +98,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1351b110b..75d6d960d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -213,6 +213,7 @@ struct VulkanDevice::State { bool samplerAnisotropy = false; bool textureCompressionBC = false; bool samplerFilterMinmax = false; + bool fragmentShaderPixelInterlock = false; // VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available. bool timelineSemaphores = false; bool computeWave32 = false; @@ -750,6 +751,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE; } + VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT interlockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT}; + if (hasExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &interlockFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->fragmentShaderPixelInterlock = interlockFeatures.fragmentShaderPixelInterlock == VK_TRUE; + } + interlockFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT}; + interlockFeatures.fragmentShaderPixelInterlock = VK_TRUE; VkPhysicalDeviceShaderClockFeaturesKHR clockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR}; if (hasExtension(VK_KHR_SHADER_CLOCK_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &clockFeatures}; @@ -763,6 +772,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric"); } + if (state->fragmentShaderPixelInterlock) { + deviceExtensions.push_back(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT); + state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock"); + } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityShaderClockKHR); @@ -990,6 +1004,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u barycentricFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &barycentricFeatures; } + if (state->fragmentShaderPixelInterlock) { + interlockFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &interlockFeatures; + } if (state->shaderClock) { clockFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &clockFeatures; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index db45a0707..f675a29da 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -161,6 +161,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), + .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, .targetExportMapping = exportMappings }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 883c9a8ea..2aab56ef5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -207,6 +207,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mesh && capability == spv::CapabilityMeshShadingEXT; + const bool isInterlockCapability = + fragment && + capability == spv::CapabilityFragmentShaderPixelInterlockEXT; + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || @@ -241,6 +245,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isBdaCapability || isTessellationCapability || isMeshCapability || + isInterlockCapability || isFeatureCapability || isDescriptorIndexingCapability || (imageInt64Atomics && (capability == spv::CapabilityInt64Atomics || capability == spv::CapabilityInt64ImageEXT)), @@ -256,6 +261,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto end = std::find(text, text + bytes.size(), '\0'); Require(end != text + bytes.size(), "unterminated SPIR-V extension"); const std::string_view extension(text, static_cast(end - text)); + if (extension == "SPV_EXT_fragment_shader_interlock") { + Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader"); + break; + } if (extension == "SPV_KHR_fragment_shader_barycentric") { Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; @@ -376,7 +385,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (fragment) { - for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater), "unsupported fragment execution mode"); + for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; bool push = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 959031d35..c265ab63c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -64,7 +64,7 @@ constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. -constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u); +constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u); constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t ScanModeMask = ~2u; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9e2bdf6b1..c24cfa0c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -10,6 +10,7 @@ #include "SceShaders.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "CacheKey.hpp" +#include "BdaAbi.hpp" #include #include #include @@ -1935,6 +1936,141 @@ void conservativeZExportTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode"); } +constexpr std::array OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u}; +constexpr std::array OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + +AgcDriver::QueueState orderedPixelQueue(std::uint32_t shaderControl) { + auto queue = makeState(); + queue.context[0x1b3] = 0x2u; + queue.context[0x1b4] = 0x2u; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest orderedPixelRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = OrderedPixelCode) { + const auto queue = orderedPixelQueue(shaderControl); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +std::string interlockShape(std::span words) { + std::string shape; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilityFragmentShaderPixelInterlockEXT) shape += "capability "; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_fragment_shader_interlock") shape += "extension "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModePixelInterlockOrderedEXT) shape += "ordered "; + if (op == spv::OpBeginInvocationInterlockEXT) shape += "begin "; + if (op == spv::OpEndInvocationInterlockEXT) shape += "end "; + } + return shape; +} + +std::string interlockExits(std::span words, std::size_t& kills) { + struct Block { + std::vector ops; + std::vector next; + }; + std::map blocks; + std::uint32_t entry = 0, function = 0, label = 0, first = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + const auto end = at + (words[at] >> 16u); + if (op == spv::OpEntryPoint) entry = words[at + 2]; + if (op == spv::OpFunction) { + function = words[at + 2]; + label = 0; + } + if (op == spv::OpLabel) label = words[at + 1]; + if (op == spv::OpLabel && function == entry && first == 0) first = label; + if (function != entry) { + if (op == spv::OpBeginInvocationInterlockEXT || op == spv::OpEndInvocationInterlockEXT || op == spv::OpKill) return "an interlock instruction or OpKill outside the entry point"; + continue; + } + if (label == 0) continue; + auto& block = blocks[label]; + block.ops.push_back(op); + if (op == spv::OpBranch) block.next = {words[at + 1]}; + if (op == spv::OpBranchConditional) block.next = {words[at + 2], words[at + 3]}; + if (op == spv::OpSwitch) { + block.next = {words[at + 2]}; + for (auto target = at + 4; target < end; target += 2) block.next.push_back(words[target]); + } + } + std::map state{{first, 0}}; + std::vector work{first}; + while (!work.empty()) { + const auto current = work.back(); + work.pop_back(); + auto stage = state[current]; + for (const auto op : blocks[current].ops) { + if (op == spv::OpBeginInvocationInterlockEXT && stage++ != 0) return "block " + std::to_string(current) + " begins the interlock twice"; + if (op == spv::OpEndInvocationInterlockEXT && stage++ != 1) return "block " + std::to_string(current) + " ends the interlock outside it"; + if ((op == spv::OpKill || op == spv::OpReturn || op == spv::OpReturnValue || op == spv::OpUnreachable || op == spv::OpTerminateInvocation) && stage != 2) return "an exit in block " + std::to_string(current) + " skips OpEndInvocationInterlockEXT"; + if (op == spv::OpKill) ++kills; + } + for (const auto next : blocks[current].next) { + const auto [known, added] = state.emplace(next, stage); + if (!added && known->second != stage) return "block " + std::to_string(next) + " is reached both inside and outside the interlock"; + if (added) work.push_back(next); + } + } + return {}; +} + +void orderedPixelShaderTests() { + const std::array interlock{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT}; + const std::array plain{spv::CapabilityShader}; + for (const auto shaderControl : {0x30640u, 0x30600u}) { + const auto queue = orderedPixelQueue(shaderControl); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl)); + Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports).orderedPixelShader, "PRIMITIVE_ORDERED_PIXEL_SHADER did not reach the pixel stage"); + } + for (const auto shaderControl : {0x130600u, 0x430600u}) { + const auto queue = orderedPixelQueue(shaderControl); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted POPS_OVERLAP_NUM_SAMPLES in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + } + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + for (const auto& [shaderControl, expected] : std::array, 2>{{{0x30600u, "capability extension ordered begin end "}, {0x20600u, ""}}}) { + const auto request = orderedPixelRequest(shaderControl, interlock); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(replayed.request.context.pixel->orderedPixelShader == (shaderControl == 0x30600u), "the serialized request lost PRIMITIVE_ORDERED_PIXEL_SHADER"); + const auto result = ShaderRecompiler::Recompile(request); + Require(interlockShape(result.spirv.Words()) == expected && interlockShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong pixel interlock: " + interlockShape(result.spirv.Words())); + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + keys.insert(key); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &result, 0}}}; + AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } + Require(keys.size() == 2, "the recompile cache key ignores PRIMITIVE_ORDERED_PIXEL_SHADER"); + const std::array bdaInterlock{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array bdaExtensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + auto faulting = orderedPixelRequest(0x30600u, bdaInterlock, OrderedBdaPixelCode); + faulting.target.supportedExtensions = bdaExtensions; + faulting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + std::size_t kills = 0; + const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills); + Require(exits.empty() && kills == 6u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill, not the valid-mask kill and 5 BDA fault kills" : exits)); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); +} + void pixelParameterSlotTests() { using AgcDriver::Graphics::CompiledShader; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; @@ -2325,6 +2461,7 @@ int main() { ZExportTests(); DepthBoundsBiasTests(); conservativeZExportTests(); + orderedPixelShaderTests(); DisabledColorTests(); CompactedExportTests(); metadataPassTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c49b53c1b..8fcc17e48 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -678,7 +678,7 @@ void verifyPixelRequestSerialization() { value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY, value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, - value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, + value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, value.targetOutputMode, value.targetExportMapping); }; const std::array code{0xbf810000u}; @@ -718,6 +718,7 @@ void verifyPixelRequestSerialization() { .earlyZ = true, .executeOnNoop = true, .conservativeZExport = ConservativeZExport::GreaterThanZ, + .orderedPixelShader = true, .targetOutputMode = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u} }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; @@ -756,19 +757,19 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQQoAAAA=", "new requests did not use serialization version 10"); - constexpr std::size_t mappingOffset = 8u + 37u + 18u + 162u; + require(requestPrefix(encoded, 8u) == "NVNQQQsAAAA=", "new requests did not use serialization version 11"); + constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; for (std::size_t bytes = 0; bytes < 8u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-8 pixel mapping was accepted"); + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-11 pixel mapping was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQsAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQwAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; - static constexpr std::array legacyPixelRequests{ + static constexpr std::array legacyPixelRequests{ "NVNQQQEAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA" @@ -809,6 +810,16 @@ void verifyLegacyPixelRequests() { "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAsAAAAAAAAAgAAAAAAAAQ==", + "NVNQQQkAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAsAAAAAAAAAgAAAAAAAAQAAAAA=", + "NVNQQQoAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAsAAAAAAAAAgAAAAAAAAQAAAAA=", }; const RequestSerializer serializer; for (std::size_t index = 0; index < legacyPixelRequests.size(); ++index) { @@ -820,6 +831,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); + 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, "legacy request trailer was misread"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp new file mode 100644 index 000000000..59257d144 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp @@ -0,0 +1,239 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t Layers = 64; +constexpr std::uint32_t PixelInterlockCapability = 5378; +constexpr std::size_t GuestBytes = 65536; +constexpr std::uint32_t FaultColumns = 8; +alignas(256) std::array Pixels{}; +alignas(256) std::array Counters{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e040f00, 0x7e060f01, 0x34060686, 0x4a040702, 0x34040482, + 0xe030d000, 0x80000402, 0xbf8c3f70, 0x4a080881, 0xe0705000, 0x80000402, + 0xbbfd0000, 0xbf900007, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array FaultPixelCode{ + 0x7e040f00, 0x7e060f01, 0x36100487, 0x34060686, 0x4a040702, 0x34040482, 0x7e0a02ff, 0x00008000, + 0x7e0c02ff, 0x00010000, 0x7d841080, 0x02160d05, 0x4a161702, 0xdc319000, 0x0a04000b, 0xe030d000, + 0x80000402, 0xbf8c3f70, 0x4a080881, 0x4a08090a, 0xe0705000, 0x80000402, 0xdce08000, 0x00040402, + 0xbbfd0000, 0xbf900007, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +class GuestCounters { +public: + GuestCounters() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, 2 * GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestBytes, 2 * GuestBytes)); +#endif + Require(block != nullptr, "pixel interlock: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true); + } + + ~GuestCounters() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestCounters(const GuestCounters&) = delete; + GuestCounters& operator=(const GuestCounters&) = delete; + + void Clear() { std::memset(block, 0, 2 * GuestBytes); } + std::uint64_t Address() const { return reinterpret_cast(block); } + std::uint32_t Word(std::size_t index) const { + std::uint32_t value = 0; + std::memcpy(&value, block + index * 4u, sizeof(value)); + return value; + } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +const std::vector>& Triangles() { + static const auto triangles = [] { + std::vector> vertices; + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + vertices.push_back({-1.0f, -1.0f, 0.5f, 1.0f}); + vertices.push_back({3.0f, -1.0f, 0.5f, 1.0f}); + vertices.push_back({-1.0f, 3.0f, 0.5f, 1.0f}); + } + return vertices; + }(); + return triangles; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span pixelCode, std::span pixelUserData) { + Pixels.fill(std::byte{0}); + Counters.fill(0); + const auto target = device.Target(); + const auto& triangles = Triangles(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.executeOnNoop = true; + pixel.orderedPixelShader = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + {0, 0, vertexPush, 128 - vertexPush} + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, vertexPush} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::vector CounterUserData() { + std::vector userData(4, 0u); + const auto counters = BufferDescriptor(Counters.data(), 0u, static_cast(sizeof(Counters))); + std::copy(counters.begin(), counters.end(), userData.begin()); + return userData; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + Draw(device, waveSize, PixelCode, CounterUserData()); +} + +void DrawFault(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, GuestCounters& guest) { + const auto what = "pixel interlock wave" + std::to_string(waveSize) + " BDA fault"; + guest.Clear(); + auto pixelUserData = CounterUserData(); + pixelUserData.push_back(static_cast(guest.Address())); + pixelUserData.push_back(static_cast(guest.Address() >> 32u)); + std::string failure; + try { + Draw(device, waveSize, FaultPixelCode, pixelUserData); + } catch (const std::exception& error) { + failure = error.what(); + } + device.WaitIdle(); + Require(failure.find("BDA access failed") != std::string::npos && failure.find("reason=1") != std::string::npos, what + ": the draw reported \"" + failure + "\", not an unmapped read"); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto expected = pixel % FaultColumns == 0 ? 0u : Layers; + Require(guest.Word(pixel) == expected, what + ": pixel " + std::to_string(pixel) + " counted " + std::to_string(guest.Word(pixel)) + " of " + std::to_string(expected) + " overlapping read-modify-writes"); + Require(guest.Word(GuestBytes / 8u + pixel) == 0u && guest.Word(GuestBytes / 4u + pixel) == 0u, what + ": pixel " + std::to_string(pixel) + " stored into its read range"); + } +} + +void Check(std::uint32_t waveSize, std::uint32_t pass) { + const auto what = "pixel interlock wave" + std::to_string(waveSize) + " pass " + std::to_string(pass); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Pixels[pixel * 4u + channel] == std::byte{255}, what + ": missing color at pixel " + std::to_string(pixel)); + } + Require(Counters[pixel] == Layers, what + ": pixel " + std::to_string(pixel) + " counted " + std::to_string(Counters[pixel]) + " of " + std::to_string(Layers) + " overlapping read-modify-writes"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto capabilities = device->Target().supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), PixelInterlockCapability) == capabilities.end()) { + std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + GuestCounters guest; + for (const auto waveSize : {64u, 32u}) { + for (std::uint32_t pass = 0; pass < 3; ++pass) { + Draw(*device, waveSize); + Check(waveSize, pass); + } + DrawFault(*device, waveSize, guest); + Draw(*device, waveSize); + Check(waveSize, 3); + } + std::printf("pixel interlock tests passed: 10 draws of %u overlapping triangles over %u pixels, 2 of them with a killed BDA fault in every %uth column\n", Layers, Width * Height, FaultColumns); + 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 003ed1644..2250648cd 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -142,6 +142,7 @@ private: append(key, value.earlyZ); append(key, value.executeOnNoop); append(key, value.conservativeZExport); + append(key, value.orderedPixelShader); append(key, value.targetOutputMode); append(key, value.targetExportMapping); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index cfcbf8af3..93f9c24ca 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -305,6 +305,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.earlyZ); writer.WriteBool(info.executeOnNoop); writer.WriteU8(static_cast(info.conservativeZExport)); + writer.WriteBool(info.orderedPixelShader); for (const std::uint8_t value : info.targetOutputMode) { writer.WriteU8(value); } @@ -341,6 +342,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.earlyZ = reader.ReadBool(); info.executeOnNoop = reader.ReadBool(); if (version >= 7u) info.conservativeZExport = readConservativeZExport(reader); + if (version >= 11u) info.orderedPixelShader = reader.ReadBool(); for (std::uint8_t& value : info.targetOutputMode) { value = reader.ReadU8(); } @@ -697,7 +699,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(10u); + writer.WriteU32(11u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -723,7 +725,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 > 10u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 11u) 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); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index befe1cce7..21162a73d 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -185,6 +185,7 @@ struct ShaderPixelInputInfo { bool psEarlyZ = false; bool psExecuteOnNoop = false; ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; + bool psOrderedPixelShader = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 7336d63ad..a01bfa2b7 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -127,6 +127,7 @@ struct ShaderPixelStageInfo { bool earlyZ; bool executeOnNoop; ConservativeZExport conservativeZExport; + bool orderedPixelShader; std::array targetOutputMode; std::array targetExportMapping; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 7d8f0874c..e266e8a90 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -90,6 +90,7 @@ DppTargetLane EmitDppRowRotateRightTargetLane(SpirvEmitterState& state, std::uin DppTargetLane EmitDppMirrorTargetLane(SpirvEmitterState& state, std::uint32_t subid, bool halfRow); DppTargetLane EmitDppTargetLane(SpirvEmitterState& state, std::uint32_t control); std::uint32_t InputVariableForKind(const SpirvEmitterState& state, StageInputKind kind); +bool OrderedPixelShader(const SpirvEmitterState& state); const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location); std::uint32_t EmitVertexParameterComponentU32(SpirvEmitterState& state, const SpirvInputBinding& input, std::uint32_t component); std::uint32_t EmitInputComponentU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index ad34e63e2..411f7fc0a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -92,6 +92,9 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) { if (state.bdaStopValue != 0) { state.module.AddFunction(spv::OpReturnValue, state.bdaStopValue); } else { + if (OrderedPixelShader(state)) { + state.module.AddFunction(spv::OpEndInvocationInterlockEXT); + } state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); } EmitLabel(state, next); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 496f4658d..2a6b3e1bc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -88,6 +88,9 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { RecordBdaFault(state, BdaConstant(state, state.program.Resources().shaderHash), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); }); } + if (OrderedPixelShader(state)) { + state.module.AddFunction(spv::OpEndInvocationInterlockEXT); + } EmitKillIfPixelValidMaskInactive(state); state.module.AddFunction(spv::OpReturn); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 72f796336..f2e2bb092 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -151,6 +151,10 @@ namespace ShaderRecompiler return 0; } + bool OrderedPixelShader(const SpirvEmitterState& state) { + return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psOrderedPixelShader; + } + const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location) { for (const auto& input : state.inputs) { if (input.kind == StageInputKind::Parameter && input.location == location) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 85e80ae10..49f70b1f3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -179,6 +179,14 @@ void DefineModule(SpirvEmitterState& state) { if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } + if (pixel.psOrderedPixelShader) { + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) { + throw std::runtime_error("a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); + } + state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT); + state.module.EmitExtension("SPV_EXT_fragment_shader_interlock"); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModePixelInterlockOrderedEXT); + } } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupQuadsKHR); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1fb490ff1..b913c9577 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -927,8 +927,14 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } + if (OrderedPixelShader(state)) { + state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); + } EmitMemoryOffsets(state); if (program.BlockOrder().empty()) { + if (OrderedPixelShader(state)) { + state.module.AddFunction(spv::OpEndInvocationInterlockEXT); + } if (state.pixelValidMaskVariable != 0u) { const auto maskValue = state.module.AllocateId(); const auto active = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 750667621..4dd7cc265 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -139,6 +139,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; pixelStorage.psConservativeZExport = pixel.conservativeZExport; + pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dc3cb67a7..d18f4b3de 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -274,6 +274,9 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink. - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. +- [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. +- [POPS collision SGPRs](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. +- [Image loads](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [Descriptor pool chain](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page