From 72518f64fbc3fc9a22491a4bcaeea34f5033d0f7 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sat, 3 Oct 2026 11:07:44 +0200 Subject: [PATCH] feat(agc): single-sample sample masks as a per-quad-pixel discard With one sample, PA_SC_AA_MASK leaves one coverage bit per pixel of the 2x2 quad, so a draw can cover only some pixels of every quad (GTA V renders a target one quarter of the pixels at a time this way). The pixel stage info carries that 4-bit mask; a fragment shader whose mask is not full starts by killing the invocation unless its gl_FragCoord quad position is covered, and runs without early fragment tests. The mask is part of the recompile cache key and the serialized request (version 24). Sample masks of multisampled draws stay refused. --- .../Graphics/src/ShaderInputState.cpp | 6 +- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 16 +++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 39 +++++- .../tests/execution/QuadPixelMask.cpp | 115 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../Optimization/ShaderStageInputInfo.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 2 +- core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 17 +++ .../src/ShaderInputInfoBuilder.cpp | 1 + 13 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 238d8bcc19..7c51b6a75a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -146,6 +146,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; + const auto aaMask = read(context, 0x30e, RegisterBank::Context); + const auto aaMaskBottom = read(context, 0x30f, RegisterBank::Context); + const auto quadPixelMask = static_cast((aaMask & 1u) | (((aaMask >> 16u) & 1u) << 1u) | ((aaMaskBottom & 1u) << 2u) | (((aaMaskBottom >> 16u) & 1u) << 3u)); const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ .interpolatorCount = inputNum, @@ -173,7 +176,8 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, .targetExportMapping = nullProgram ? std::array{} : exportMappings, - .stencilRefExportEnable = stencilRefExportEnable + .stencilRefExportEnable = stencilRefExportEnable, + .quadPixelMask = quadPixelMask }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index d9bd9f99a4..f673966763 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -816,7 +816,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported"); Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported"); - Require(read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu, "sample masks are unsupported"); + Require((read(cx, 0x2f8) & 7u) == 0 || (read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu), "sample masks of multisampled draws are unsupported"); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); @@ -1391,7 +1391,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported"); if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word); std::uint32_t other = 0; - if (value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks are unsupported"); + if (value(cx, 0x2f8, word) && (word & 7u) != 0 && value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks of multisampled draws are unsupported"); if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word); if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ca7dfcf44b..82c9b731e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2780,6 +2780,21 @@ void framebufferLayerTests() { } } +void quadPixelMaskTests() { + auto queue = makeState(); + queue.context[0x30e] = 0xffff0000u; + queue.context[0x30f] = 0u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a single-sample quad pixel mask was refused by the precheck"); + Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0x2u, "PA_SC_AA_MASK did not decode to the covered quad pixel"); + queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0xfu, "full sample masks did not cover the whole quad"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -6723,6 +6738,7 @@ int main(int argc, char** argv) { clipDistanceTests(); framebufferLayerTests(); UnwrittenSampleLocationTests(); + quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 261c21e612..bf4b264415 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2590,6 +2590,22 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(vertex, unclipped); require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key"); } + { + RecompileRequest fragment{}; + fragment.shader = {ShaderStage::Fragment, 0x10000u, code, 0, {}}; + fragment.context.waveSize = 64; + ShaderPixelStageInfo info{}; + info.quadPixelMask = 0x2u; + fragment.context.pixel = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(back.request.context.pixel.has_value() && back.request.context.pixel->quadPixelMask == 0x2u, "the quad pixel mask was lost in serialization"); + std::vector masked; + RecompileCacheKey::Build(fragment, masked); + fragment.context.pixel->quadPixelMask = 0xfu; + std::vector full; + RecompileCacheKey::Build(fragment, full); + require(masked != full, "the quad pixel mask is not part of the recompile cache key"); + } std::vector key; RecompileCacheKey::Build(request, key); const auto first = key; @@ -2970,7 +2986,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRsAAAA=", "new requests did not use serialization version 27"); + require(requestPrefix(encoded, 8u) == "NVNQQRwAAAA=", "new requests did not use serialization version 28"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3085,10 +3101,10 @@ void verifyPixelRequestSerialization() { require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); } constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 24u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag was accepted"); + for (std::size_t bytes = 0; bytes < 25u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, or quad pixel mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRwAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR0AAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3213,12 +3229,23 @@ void verifyVertexInfoVersion19() { std::size_t allBitsOffset = 0; while (allBitsOffset < version26Pixel.size() && version26Pixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); - version26Pixel.erase(version26Pixel.begin() + static_cast(allBitsOffset)); + version26Pixel.erase(version26Pixel.begin() + static_cast(allBitsOffset), version26Pixel.begin() + static_cast(allBitsOffset + 2u)); version26Pixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(version26Pixel)); require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag"); require(serializer.Serialize(replay26.request) == encodedPixel, "a version 26 request misread the fields after its dithered coverage flag"); - legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 3u)); + auto maskedPixel = pixel; + maskedPixel.context.pixel->quadPixelMask = 0x2u; + const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); + std::size_t maskOffset = 0; + while (maskOffset < legacyPixel.size() && legacyPixel[maskOffset] == maskedBytes[maskOffset]) ++maskOffset; + require(maskOffset == flagOffset + 3u && maskOffset < legacyPixel.size(), "the quad pixel mask is not the byte after the front-face all-bits flag"); + auto version27 = maskedBytes; + version27.erase(version27.begin() + static_cast(maskOffset)); + version27[4] = 27u; + const auto replay27 = serializer.Deserialize(encodeBase64(version27)); + require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or misread the fields after it"); + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 4u)); legacyPixel.resize(legacyPixel.size() - sizeof(std::uint64_t)); for (const auto version : {19u, 20u, 21u, 22u}) { auto versionPixel = legacyPixel; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp new file mode 100644 index 0000000000..73c7e137de --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp @@ -0,0 +1,115 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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; +alignas(256) std::array Pixels{}; +alignas(256) std::array, 3> Triangle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +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}; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint8_t quadPixelMask) { + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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, {}}, + {64, 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.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + pixel.quadPixelMask = quadPixelMask; + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {64, 0, {}, std::nullopt, pixel, 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, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint8_t quadPixelMask) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool expected = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0; + const bool written = std::to_integer(Pixels[(y * Width + x) * 4u]) == 255u; + Require(written == expected, "quad pixel mask 0x" + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (written ? "written" : "not written")); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint8_t mask : {std::uint8_t{0xf}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + Draw(*device, mask); + Check(mask); + } + std::puts("quad pixel mask 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 2ae9138a4f..da2e8369b7 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -191,6 +191,7 @@ private: append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); append(key, value.ditheredAlphaToCoverage); + append(key, value.quadPixelMask); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 4cd8c2c3d2..a760eb0831 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -340,6 +340,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.layerExported); writer.WriteBool(info.ditheredAlphaToCoverage); writer.WriteBool(info.frontFaceAllBits); + writer.WriteU8(info.quadPixelMask); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -393,6 +394,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 23u) info.layerExported = reader.ReadBool(); if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool(); if (version >= 27u) info.frontFaceAllBits = reader.ReadBool(); + if (version >= 28u) info.quadPixelMask = reader.ReadU8(); 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) | @@ -764,7 +766,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(27u); + writer.WriteU32(28u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -797,7 +799,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 > 27u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 28u) 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 12ddcbcdb7..44844c85f8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -181,6 +181,7 @@ struct ShaderPixelInputInfo { bool psOrderedPixelShader = false; bool psAlphaToCoverage = false; bool psDitheredAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 5fd74492b7..213cb420a6 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -237,7 +237,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->quadPixelMask != 0xf) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e3c14236fd..b93b873ac6 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -154,6 +154,7 @@ struct ShaderPixelStageInfo { RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; bool ditheredAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 92b7f77ec2..8c9d445e96 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -196,7 +196,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage) { + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && pixel.quadPixelMask == 0xf) { 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 c2dd9ee2cb..b7bd389110 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1180,6 +1180,23 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } EmitMemoryOffsets(state); + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->quadPixelMask != 0xfu) { + const auto coordinate = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u))); + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, state.inputInfo.pixel->quadPixelMask), quad), ConstantU32(state, 1u)); + const auto uncovered = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u)); + const auto killLabel = state.module.AllocateId(); + const auto mergeLabel = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, mergeLabel, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, uncovered, killLabel, mergeLabel); + EmitLabel(state, killLabel); + state.module.AddFunction(spv::OpKill); + EmitLabel(state, mergeLabel); + } if (program.BlockOrder().empty()) { if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index dde903ea3f..972a73a2c4 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -148,6 +148,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage"); pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; + pixelStorage.quadPixelMask = pixel.quadPixelMask; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result;