From 22d662eda76145501e9afa065932fac4f93dbfa8 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 19:39:43 +1300 Subject: [PATCH] fix(agc): export the stencil reference from pixel shaders with VK_EXT_shader_stencil_export --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 5 + .../Graphics/src/ShaderInputState.cpp | 6 +- .../Graphics/src/ShaderValidation.cpp | 13 +- .../libSceAgcDriver/Graphics/src/State.cpp | 17 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 126 ++++++++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 16 +- .../tests/execution/StencilRefExport.cpp | 159 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../IrMetadata/StageIO.hpp | 1 + .../Optimization/ShaderStageInputInfo.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 3 + core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 3 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 10 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 6 + .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/TechnicalDebt.md | 1 + tools/tests/test_check_skips.py | 1 + 22 files changed, 363 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.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 14090d411..7b959293e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -64,6 +64,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); + mix(pixel.stencilRefExportEnable); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); for (const auto value : pixel.targetExportPacking) mix(static_cast(value)); 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 3e1fd6a77..6dec41300 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.orderedPixelShader != q.orderedPixelShader || 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.stencilRefExportEnable != q.stencilRefExportEnable || 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 d24dbbc56..0f03fb022 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1095,6 +1095,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT); state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock"); } + if (hasExtension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityStencilExportEXT); + state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export"); + } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityShaderClockKHR); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 3e89ca766..61cfa6b42 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -142,6 +142,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ @@ -163,12 +164,13 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, + .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable, .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, - .targetExportMapping = nullProgram ? std::array{} : exportMappings + .targetExportMapping = nullProgram ? std::array{} : exportMappings, + .stencilRefExportEnable = stencilRefExportEnable }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index d30076237..029c89209 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -137,7 +137,7 @@ struct Module { Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { - Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); + Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); } } }; @@ -232,6 +232,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityFragmentShaderPixelInterlockEXT; + const bool isStencilExportCapability = + fragment && + capability == spv::CapabilityStencilExportEXT; + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || @@ -273,6 +277,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isMeshCapability || isGeometryCapability || isInterlockCapability || + isStencilExportCapability || isFeatureCapability || isDescriptorIndexingCapability || ((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) || @@ -293,6 +298,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader"); break; } + if (extension == "SPV_EXT_shader_stencil_export") { + Require(fragment, "SPV_EXT_shader_stencil_export 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; @@ -420,7 +429,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeOutputVertices, {3u}); Require(module.modes.size() == 4, "unsupported geometry 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 || name == spv::ExecutionModePixelInterlockOrderedEXT), "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 || name == spv::ExecutionModeStencilRefReplacingEXT), "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 ce50da558..2be0ff53a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -119,9 +119,11 @@ bool zFormatSupported(std::uint32_t format) { std::uint32_t shaderControlMask(std::uint32_t zFormat) { constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t stencilTestValExportEnable = 0x2u; constexpr std::uint32_t maskExportEnable = 0x100u; auto mask = ShaderControlMask; if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; + if (zFormat == 2u || zFormat == 9u) mask &= ~stencilTestValExportEnable; if (zFormat == 9u) mask &= ~maskExportEnable; return mask; } @@ -154,7 +156,7 @@ bool depthPlanesAbsent(const Registers& cx) { return z != cx.end() && stencil != cx.end() && (z->second & 3u) == 0 && (stencil->second & 1u) == 0; } -VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport) { VkStencilOpState face{}; face.compareOp = static_cast(compare); face.compareMask = (refMask >> 8u) & 0xffu; @@ -172,6 +174,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint case 2: case 3: case 4: { + if (testValExport) { + if (op != 3) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the test value is unsupported with a pixel shader that exports the stencil test value"); + return VK_STENCIL_OP_REPLACE; + } const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; if (((reference ^ value) & fixed & face.writeMask) != 0) { std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); @@ -203,7 +209,7 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint return face; } -void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { +void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; @@ -223,6 +229,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) const bool depthReadOnly = (view & 0x01000000u) != 0; const bool stencilReadOnly = (view & 0x02000000u) != 0; Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); + Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); @@ -274,8 +281,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) result.stencilBack = clear; } else if (result.stencilTest) { const auto ops = read(cx, 0x10b); - result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); - result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport); + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport) : result.stencilFront; } } @@ -667,7 +674,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; const auto tests = planeTests(cx, depthControl); if (clear || (tests != 0 && depthSurfaceBound(cx))) { - decodeDepth(cx, depthControl, result); + decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..ae0f814f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3688,6 +3688,131 @@ void orderedPixelShaderTests() { expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } +constexpr std::array StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u}; +constexpr std::array DepthStencilRefExportCode{0x7e0e02f2u, 0xf8001883u, 0x707u, 0xbf810000u}; + +AgcDriver::QueueState stencilRefExportQueue(std::uint32_t shaderControl, std::uint32_t zFormat) { + auto queue = makeState(); + queue.context[0x1b3] = 0x300u; + queue.context[0x1b4] = 0x300u; + queue.context[0x1c4] = zFormat; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest stencilRefExportRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = StencilRefExportCode) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + 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 stencilRefShape(std::span words) { + std::string shape; + std::uint32_t variable = 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); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability "; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_shader_stencil_export") shape += "extension "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) shape += "early "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) shape += "depth "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeStencilRefReplacingEXT) shape += "replacing "; + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragStencilRefEXT) { + variable = words[at + 1]; + shape += "builtin "; + } + if (op == spv::OpStore && variable != 0 && words[at + 1] == variable) shape += "store "; + } + return shape; +} + +void stencilRefExportTests() { + for (const auto shaderControl : {0x52u, 0x12u, 0x02u, 0x03u}) { + for (const auto zFormat : {2u, 9u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with Z format " + std::to_string(zFormat)); + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + Require(pixel.stencilRefExportEnable, "STENCIL_TEST_VAL_EXPORT_ENABLE did not reach the pixel stage"); + Require(!pixel.earlyZ, "a pixel shader that exports the stencil reference was given early tests"); + } + for (const auto zFormat : {0u, 1u, 3u, 7u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat)); + } + } + for (const auto shaderControl : {0x54u, 0x56u}) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + } + Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); + const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; + const std::array plain{spv::CapabilityShader}; + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + const std::array, 4> cases{{{0x52u, "capability extension replacing builtin store "}, {0x12u, "capability extension replacing builtin store "}, {0x50u, ""}, {0x10u, "early "}}}; + for (const auto& [shaderControl, expected] : cases) { + const auto request = stencilRefExportRequest(shaderControl, stencilExport); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(replayed.request.context.pixel->stencilRefExportEnable == ((shaderControl & 2u) != 0), "the serialized request lost STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto result = ShaderRecompiler::Recompile(request); + Require(stencilRefShape(result.spirv.Words()) == expected && stencilRefShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong stencil reference export: " + stencilRefShape(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() == cases.size(), "the recompile cache key ignores STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto both = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x53u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + Require(both == "capability extension depth replacing builtin store ", "a depth and stencil reference export declared the wrong outputs: " + both); + const auto depthOnly = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x51u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + Require(depthOnly == "depth ", "an MRTZ stencil channel without STENCIL_TEST_VAL_EXPORT_ENABLE declared the wrong outputs: " + depthOnly); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain))); }, "VK_EXT_shader_stencil_export"); + auto queue = stencilRefExportQueue(0x52u, 2u); + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x10d] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilTest && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.passOp == VK_STENCIL_OP_REPLACE, "a stencil reference export changed the REPLACE_TEST decode"); + for (const auto ops : {0x00020020u, 0x00040040u, 0x00000200u, 0x00000004u}) { + queue.context[0x10b] = ops; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "exports the stencil test value"); + } + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff02; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP, "a stencil reference export changed the increment decode"); + queue.context[0x000] = 2; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value"); + queue.context[0x203] = 0x50u; + static_cast(AgcDriver::Graphics::DecodeState(queue)); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -4705,6 +4830,7 @@ int main(int argc, char** argv) { DepthBoundsBiasTests(); conservativeZExportTests(); orderedPixelShaderTests(); + stencilRefExportTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1526845de..bf1f92b8c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1542,7 +1542,7 @@ void verifyPixelRequestSerialization() { 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.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1586,6 +1586,7 @@ void verifyPixelRequestSerialization() { }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; pixel.alphaToCoverage = true; + pixel.stencilRefExportEnable = true; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1614,6 +1615,10 @@ void verifyPixelRequestSerialization() { withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutCoverage), "alpha-to-coverage did not change the static shader identity"); + auto withoutStencilRef = request; + withoutStencilRef.context.pixel->stencilRefExportEnable = false; + RecompileCacheKey::Build(withoutStencilRef, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity"); } request.context.pixel.reset(); request.shader.stage = ShaderStage::Vertex; @@ -1625,12 +1630,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 (std::size_t bytes = 0; bytes < 19u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage or stencil reference export 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"); } } @@ -1720,6 +1725,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); require(!pixel.alphaToCoverage, "a legacy request enabled alpha-to-coverage"); + require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference"); 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"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp new file mode 100644 index 000000000..ac0318165 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp @@ -0,0 +1,159 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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 = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PlainPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array ColumnPixelCode{ + 0x7e040f00, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array RowPixelCode{ + 0x7e040f01, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array, 3> Vertices{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}, +}}; + +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}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { + const AgcDriver::Registers context{ + {0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, + }; + return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil) { + Pixels.fill(Kept); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.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()); + + const std::vector pixelUserData(8, 0u); + 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, PixelInfo(shaderControl), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + 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.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + state.stencilTest = true; + state.stencilFront = stencil; + state.stencilBack = stencil; + 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(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::function& drawn, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = drawn(x, y) ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "stencil-rejected")); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityStencilExportEXT)) { + std::puts("skipped, the device lacks VK_EXT_shader_stencil_export"); + return VulkanTestSkipped; + } + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + const auto equal = [](std::uint32_t reference) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; }; + Draw(*device, ColumnPixelCode, exported, replace); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the draw that replaces the stencil with the exported column"); + for (const auto column : {0u, 5u, 37u, 63u}) { + Draw(*device, PlainPixelCode, plain, equal(column)); + Check([&](std::uint32_t x, std::uint32_t) { return x == column; }, "stencil equal to " + std::to_string(column) + " after the exported column was stored"); + } + Draw(*device, ColumnPixelCode, exported, equal(200u)); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the exported column tested against the stored column"); + Draw(*device, RowPixelCode, exported, equal(200u)); + Check([](std::uint32_t x, std::uint32_t y) { return x == y; }, "the exported row tested against the stored column"); + Draw(*device, ColumnPixelCode, plain, equal(9u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 9u; }, "a column export without STENCIL_TEST_VAL_EXPORT_ENABLE"); + std::puts("stencil reference export 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..27816a7d9 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -177,6 +177,7 @@ private: append(key, value.conservativeZExport); append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); + append(key, value.stencilRefExportEnable); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 5a76e67bb..fe32878c4 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -325,6 +325,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { } writer.WriteBool(info.dualSourceBlend); writer.WriteBool(info.alphaToCoverage); + writer.WriteBool(info.stencilRefExportEnable); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -373,6 +374,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } if (version >= 15u) info.dualSourceBlend = reader.ReadBool(); if (version >= 16u) info.alphaToCoverage = reader.ReadBool(); + if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -734,7 +736,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); @@ -762,7 +764,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); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 4046804ef..2c5b7c379 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -51,6 +51,7 @@ enum class StageOutputKind { Mrt, Depth, SampleMask, + StencilRef, PointSize, ClipDistance, CullDistance, diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 32300b030..ba65e3242 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -167,6 +167,7 @@ struct ShaderPixelInputInfo { bool psPixelKillEnable = false; bool psDepthExportEnable = false; bool psSampleMaskExportEnable = false; + bool psStencilRefExportEnable = false; bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 00bde0c18..7ea25b14f 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -408,6 +408,9 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x4u) != 0u && inputInfo.pixel->psSampleMaskExportEnable) { AddOutput(info, StageOutputKind::SampleMask, 0, 0, "gl_SampleMask"); } + if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x2u) != 0u && inputInfo.pixel->psStencilRefExportEnable) { + AddOutput(info, StageOutputKind::StencilRef, 0, 0, "gl_FragStencilRefARB"); + } continue; } if (exportInfo.en == 0u) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 85f9689e8..8714fc3d1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -141,6 +141,7 @@ struct ShaderPixelStageInfo { std::array targetExportPacking; bool dualSourceBlend; bool alphaToCoverage; + bool stencilRefExportEnable; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index ca69aa93c..1a63e7baa 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -171,6 +171,7 @@ struct SpirvEmitterState { std::uint32_t viewportIndexVariable = 0; std::uint32_t depthVariable = 0; std::uint32_t sampleMaskVariable = 0; + std::uint32_t stencilRefVariable = 0; std::vector inputs; std::vector outputs; std::vector interfaceVariables; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c484fa2b8..f0d347e5b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -359,6 +359,9 @@ void DefineOutputs(SpirvEmitterState& state) { case StageOutputKind::SampleMask: binding.variableId = BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); break; + case StageOutputKind::StencilRef: + binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT); + break; case StageOutputKind::Parameter: case StageOutputKind::Mrt: { const bool uintOutput = binding.kind == StageOutputKind::Mrt && state.program.Resources().stage == IrShaderStage::Pixel && binding.index < std::size(PixelInfo(state).targetOutputMode) && PixelInfo(state).targetOutputMode[binding.index] == 7u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 699290232..a6ada9eba 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -185,7 +185,15 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psAlphaToCoverage) { + if (state.stencilRefVariable != 0) { + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { + throw std::runtime_error("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks"); + } + state.module.EmitCapability(spv::CapabilityStencilExportEXT); + state.module.EmitExtension("SPV_EXT_shader_stencil_export"); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); + } + if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psStencilRefExportEnable && !pixel.psAlphaToCoverage) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1793ea2f8..51f94b566 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1298,6 +1298,12 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); state.module.AddFunction(spv::OpStore, state.depthVariable, f32); } + if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { + const auto raw = ExportRawComponent(ctx, data, 1); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); + state.module.AddFunction(spv::OpStore, state.stencilRefVariable, value); + } if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 79791905b..ce067c491 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -136,6 +136,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; pixelStorage.psDepthExportEnable = pixel.depthExportEnable; pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable; + pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable; pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f42d54e58..80dd923f7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -541,6 +541,7 @@ Throughout the project, every function at every stage either **does exactly what - 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 data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - [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.STENCIL_TEST_VAL_EXPORT_ENABLE](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none). - [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. diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 02ab77f59..d8ba15c75 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -13,6 +13,7 @@ LIMITATIONS = [ "skipped, the device lacks fragmentShaderPixelInterlock", "skipped, the device lacks geometryShader", "skipped, the device lacks shaderSubgroupClock", + "skipped, the device lacks VK_EXT_shader_stencil_export", "skipped, the device does not import guest memory", "skipped, the device imports no host memory", "skipped, the device flushes f64 subnormals without DenormPreserve",