From 03b0db05206922ecb991a631184b56be153c3db8 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 15 Oct 2026 17:39:12 +0000 Subject: [PATCH 1/4] feat(shader): load the SAMPLE_COVERAGE pixel input from gl_SampleMaskIn SPI_PS_INPUT_ENA/ADDR bit 14 (SAMPLE_COVERAGE_ENA) gives the pixel shader one VGPR, after ANCILLARY, holding the samples the primitive covers. The decoder refused it and the recompiler never loaded the VGPR. It is now loaded from gl_SampleMaskIn[0] (sharing the input the partial quad sample mask already declares). On a multisampled draw the hardware word is the whole pixel's coverage while gl_SampleMaskIn holds only the invocation's samples when the pixel shader runs per sample, so that case, and sample-rate or ancillary inputs beside it, are refused. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/ShaderInputState.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 12 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 53 +++- .../tests/execution/PixelSampleCoverage.cpp | 252 ++++++++++++++++++ .../IrMetadata/StageIO.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 4 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 17 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 9 + .../Translation/src/InstructionTranslator.cpp | 3 + .../src/ShaderInputInfoBuilder.cpp | 1 + 10 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 6a83dea572..c39338893a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -118,7 +118,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con constexpr std::uint32_t knownMask = PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::PerspectiveCentroid) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::LinearCenter) | PixelInputBit(PixelInput::LinearCentroid) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::PositionZ) | PixelInputBit(PixelInput::PositionW) | - PixelInputBit(PixelInput::FrontFace) | PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::LineStipple) | PixelInputBit(PixelInput::PositionFixedPoint); + PixelInputBit(PixelInput::FrontFace) | PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage) | PixelInputBit(PixelInput::LineStipple) | PixelInputBit(PixelInput::PositionFixedPoint); if ((activeInputs & ~knownMask) != 0) { char message[128]; std::snprintf(message, sizeof(message), "AGC graphics: unsupported SPI_PS_INPUT_ENA/ADDR bit combination (ena 0x%x addr 0x%x)", ena, addr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 36293b21a8..47e4933079 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -176,6 +176,8 @@ bool ditheredAlphaLocationsSupported(const Registers& cx) { constexpr std::uint32_t ScanModeMask = ~0x22u; constexpr std::uint32_t ScanControlMask = ~0x76033fffu; constexpr std::uint32_t PixelShaderIteratesSamples = 0x10000u; +constexpr std::uint32_t PixelSampleCoverageInput = 0x4000u; +constexpr std::uint32_t PixelSampleRateInputs = 0x2011u; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; // Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space. constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x01000000u | 0x0c000000u); @@ -461,6 +463,15 @@ constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; constexpr std::uint32_t SampleZeroRefusedInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); +bool sampleCoverageSupported(const QueueState& queue, std::uint32_t samples, bool perSampleShading) { + if (samples == 1u || PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena == queue.context.end() || addr == queue.context.end()) return true; + const auto inputs = ena->second & addr->second; + return (inputs & PixelSampleCoverageInput) == 0u || (!perSampleShading && (inputs & PixelSampleRateInputs) == 0u); +} + bool coveredCentroidSupported(const QueueState& queue) { const auto config = find(queue.context, 0x2f8); if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; @@ -1157,6 +1168,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } zero(cx, 0x293, ScanControlMask, "primitive discard or out-of-order rasterization"); result.perSampleShading = result.samples > 1 && (read(cx, 0x293) & PixelShaderIteratesSamples) != 0; + Require(sampleCoverageSupported(queue, result.samples, result.perSampleShading), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); if (depthKeepsSampleZero && result.samples > 1) { Require(rasterizesSampleZeroOnly(cx, result), "the depth target's sample count differs from the rasterizer's"); rasterizeSampleZero(result); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 8b15c61a9e..12aa55ff64 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1249,6 +1249,36 @@ std::vector pixelBuiltinsRead(std::uint32_t ena, std::uint32_t add return read; } +std::size_t sampleMaskInputs(std::uint32_t inputs, std::uint16_t quadMask) { + auto queue = makeState(); + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + pixel.quadSampleMask[0] = quadMask; + const auto source = ShaderRecompiler::PixelInputVgpr(inputs, ShaderRecompiler::PixelInput::SampleCoverage); + const std::array code{0xf800180fu, source * 0x01010101u, 0xbf810000u}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.fragmentShaderBarycentricEnabled = true; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + std::map storage; + std::vector sampleMasks; + 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::OpVariable) storage[words[at + 2]] = words[at + 3]; + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInSampleMask) sampleMasks.push_back(words[at + 1]); + } + return static_cast(std::ranges::count_if(sampleMasks, [&](std::uint32_t id) { return storage[id] == spv::StorageClassInput; })); +} + bool readsBuiltin(const std::vector& read, spv::BuiltIn builtin) { return std::find(read.begin(), read.end(), builtin) != read.end(); } @@ -1500,9 +1530,11 @@ void PixelInputLayoutTests() { queue.context[0x1b8] = 0u; pixel = decode(0x506u, 0x7afu); Require(pixel.inputAddr == 0x7afu && !pixel.posY && pixel.posZ && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 12u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionZ) == 14u, "ADDR-only inputs did not reserve their VGPRs"); - for (const auto bit : {0x8u, 0x4000u}) { - expectFailure([&] { static_cast(decode(0x2u | bit, 0x2u | bit)); }, "unsupported SPI_PS_INPUT_ENA/ADDR"); - } + expectFailure([&] { static_cast(decode(0xau, 0xau)); }, "unsupported SPI_PS_INPUT_ENA/ADDR"); + pixel = decode(0x4002u, 0x4002u); + Require(pixel.inputAddr == 0x4002u && PixelInputVgpr(pixel.inputAddr, PixelInput::SampleCoverage) == 2u, "SAMPLE_COVERAGE did not take its VGPR after the perspective center"); + Require(readsBuiltin(pixelBuiltinsRead(0x4002u, 0x4002u, 2u), spv::BuiltInSampleMask), "SAMPLE_COVERAGE is not read from gl_SampleMaskIn"); + Require(sampleMaskInputs(0x4002u, 0xffffu) == 1u && sampleMaskInputs(0x4002u, 0x0003u) == 1u, "SAMPLE_COVERAGE beside a partial quad sample mask declared gl_SampleMaskIn more than once"); pixel = decode(0x80u, 0x80u); Require(pixel.inputAddr == 0x80u && PixelInputVgpr(pixel.inputAddr, PixelInput::LineStipple) == 0u, "a pixel shader with only LINE_STIPPLE_TEX enabled was rejected"); pixel = decode(0x8082u, 0x8082u); @@ -3116,7 +3148,20 @@ void multisampleTests() { queue.context[0x293] = 0x10000u; Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "PS_ITER_SAMPLE on a 2x target was refused"); Require(DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE on a 2x target did not request sample shading"); + queue.context[0x1b3] = 0x4002u; + queue.context[0x1b4] = 0x4002u; + expectFailure([&] { DecodeState(queue); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); queue.context[0x293] = 0; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty() && !DecodeState(queue).perSampleShading, "SAMPLE_COVERAGE with center interpolation on a 2x target was refused"); + for (const auto sampleRate : {0x1u, 0x10u, 0x2000u}) { + queue.context[0x1b3] = 0x4002u | sampleRate; + queue.context[0x1b4] = 0x4002u | sampleRate; + expectFailure([&] { DecodeState(queue); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); + queue.context[0x1b4] = 0x4002u; + Require(DecodeState(queue).samples == 2, "a sample-rate input enabled but not addressed beside SAMPLE_COVERAGE decoded differently"); + } + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; queue.context[0x302] = 0x44cc; state = DecodeState(queue); Require(state.customSampleLocations && state.sampleLocationGrid, "a standard 2x pattern with its samples swapped in one pixel did not decode as a grid"); @@ -3186,7 +3231,7 @@ void multisampleTests() { Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE with sample and position inputs (PPSA27836) did not rasterize sample 0 alone"); state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid})); Require(state.samples == 1, "PS_ITER_SAMPLE with centroid inputs did not rasterize sample 0 alone"); - expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::SampleCoverage})); }, "sample count differs from the rasterizer's"); + expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::SampleCoverage})); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::PositionX})); Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE with sample-index and position inputs did not rasterize sample 0 alone"); auto iteratedStencil = withInputs(stenciled, {}); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp new file mode 100644 index 0000000000..82924510e8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -0,0 +1,252 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr 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) std::array, 3> Programs{}; +std::size_t programCount = 0; + +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}; +} + +std::span Program(std::uint32_t coverageVgpr) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + coverageVgpr); + code[at++] = 0x7e000280u | (21u << 17u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + 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, {}}, + {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.inputAddr = inputAddr; + pixel.ancillary = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0u; + pixel.hasPerspectiveCenterVgpr = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter)) != 0u; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + 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, 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)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = 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.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint32_t coverageVgpr, const std::string& what) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, 16).Bytes() == bytes, what + ": the linear target is padded"); + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.format == VK_FORMAT_R32G32B32A32_SFLOAT && color.elementBytes == 16 && color.bytes == bytes, what + ": the target decoded to the wrong format"); + Draw(device, color, inputAddr, Program(coverageVgpr)); + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{1.0f, 0.0f, 0.0f, 1.0f}; + Require(texel == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::SampleCoverage), 0u, "SAMPLE_COVERAGE alone"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center"); + Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY"); + std::puts("pixel sample coverage tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 3fbf894fd3..83690a5aaf 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -53,6 +53,7 @@ enum class StageInputKind { BaryCoordNoPerspectiveCenter, BaryCoordSmoothSample, BaryCoordNoPerspectiveSample, + SampleMask, }; constexpr StageInputKind BarycentricInputOf(StageInputKind kind) { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 4594963011..5a97337b76 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -143,6 +143,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu } case StageInputKind::Layer: case StageInputKind::SampleId: + case StageInputKind::SampleMask: case StageInputKind::RasterPrimitive: case StageInputKind::LayerExported: if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) { @@ -379,6 +380,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::SampleId: AddInput(info, kind, 0, 1, "gl_SampleID"); break; + case StageInputKind::SampleMask: + AddInput(info, kind, 0, 1, "gl_SampleMaskIn"); + break; case StageInputKind::BaryCoordSmooth: case StageInputKind::BaryCoordSmoothCenter: case StageInputKind::BaryCoordSmoothSample: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index ad0ada9b66..ebbd518a37 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -77,6 +77,7 @@ std::uint32_t BuiltInForInput(StageInputKind kind) { case StageInputKind::HelperInvocation: return spv::BuiltInHelperInvocation; case StageInputKind::Layer: return spv::BuiltInLayer; case StageInputKind::SampleId: return spv::BuiltInSampleId; + case StageInputKind::SampleMask: return spv::BuiltInSampleMask; case StageInputKind::BaryCoordSmooth: return spv::BuiltInBaryCoordKHR; case StageInputKind::BaryCoordNoPerspective: return spv::BuiltInBaryCoordNoPerspKHR; case StageInputKind::WorkgroupId: return spv::BuiltInWorkgroupId; @@ -241,6 +242,9 @@ void DefineInputs(SpirvEmitterState& state) { case StageInputKind::HelperInvocation: type = TypeBool(state); break; + case StageInputKind::SampleMask: + type = SampleMaskArrayType(state); + break; case StageInputKind::Parameter: if (state.program.Resources().stage == IrShaderStage::Vertex || state.program.Resources().stage == IrShaderStage::Local) { type = VertexParameterScalarType(state, VertexParameterScalarKind(state, input.location)); @@ -258,7 +262,7 @@ void DefineInputs(SpirvEmitterState& state) { break; } input.variableId = DefineInterfaceVariable(state, type, spv::StorageClassInput, input.debugName.c_str()); - if (input.kind == StageInputKind::Layer || input.kind == StageInputKind::SampleId) { + if (input.kind == StageInputKind::Layer || input.kind == StageInputKind::SampleId || input.kind == StageInputKind::SampleMask) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationFlat); } if (input.kind == StageInputKind::Parameter) { @@ -303,9 +307,14 @@ void DefineInputs(SpirvEmitterState& state) { } } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { - state.sampleMaskInputVariable = DefineInterfaceVariable(state, SampleMaskArrayType(state), spv::StorageClassInput, "gl_SampleMaskIn"); - state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationBuiltIn, spv::BuiltInSampleMask); - state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationFlat); + const auto read = std::find_if(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::SampleMask && input.variableId != 0u; }); + if (read != state.inputs.end()) { + state.sampleMaskInputVariable = read->variableId; + } else { + state.sampleMaskInputVariable = DefineInterfaceVariable(state, SampleMaskArrayType(state), spv::StorageClassInput, "gl_SampleMaskIn"); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationBuiltIn, spv::BuiltInSampleMask); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationFlat); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 44cc74aac8..3ad4dc9325 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -155,6 +155,15 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); return bits; } + if (kind == StageInputKind::SampleMask) { + const auto pointer = state.module.AllocateId(); + const auto value = state.module.AllocateId(); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, variable, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLoad, TypeI32(state), value, pointer); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + return bits; + } if (kind == StageInputKind::FragCoord || kind == StageInputKind::TessCoord) { const auto pointer = state.module.AllocateId(); const auto value = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 8d01811f39..220ddf13d6 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -409,6 +409,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO if (loaded(PixelInput::Ancillary)) { setVector(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); } + if (loaded(PixelInput::SampleCoverage)) { + setVector(static_cast(vgpr(PixelInput::SampleCoverage)), builtin(StageInputKind::SampleMask)); + } if (loaded(PixelInput::LineStipple)) { setVector(static_cast(vgpr(PixelInput::LineStipple)), entryIr.Constant(0u)); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 9c438ed56a..20f59866f2 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -123,6 +123,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues place(PixelInput::PositionW, pixel.posW); place(PixelInput::FrontFace, pixel.frontFace); place(PixelInput::Ancillary, pixel.ancillary); + place(PixelInput::SampleCoverage, (pixel.inputAddr & PixelInputBit(PixelInput::SampleCoverage)) != 0u); place(PixelInput::LineStipple, (pixel.inputAddr & PixelInputBit(PixelInput::LineStipple)) != 0u); place(PixelInput::PositionFixedPoint, (pixel.inputAddr & PixelInputBit(PixelInput::PositionFixedPoint)) != 0u); for (std::uint32_t i = 0; i < 8; ++i) { From 7f59934b7dd11cf32c1003c7e6a59ae8a516b9a4 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 15 Oct 2026 18:32:38 +0000 Subject: [PATCH 2/4] feat(agc): draw radeonsi line and polygon smoothing through merged mixed samples A radeonsi-style GL driver smooths lines and polygons by rasterizing four samples into a one-sample color target: PA_SC_AA_CONFIG has 4 samples, DB_EQAA.OVERRASTERIZATION_AMOUNT is log2(4), and the pixel shader reads SAMPLE_COVERAGE to scale alpha by the covered samples. The pixel is written when any of the four samples is covered. DecodeState refused the one-sample target as a sample-count mismatch. Such a draw (MSAA_ENABLE set, no depth target, no per-sample shading, every color target one-sample, OVERRASTERIZATION_AMOUNT matching the raster samples) now decodes with mergedCoverage. Its pipeline keeps the raster sample count, gives the color attachments one sample (VkAttachmentSampleCountInfoNV or the render pass) and merges coverage (VK_COVERAGE_REDUCTION_MODE_MERGE_NV). The device enables VK_NV_framebuffer_mixed_samples and VK_NV_coverage_reduction_mode when it reports that combination; elsewhere the pipeline throws. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 27 ++++++++++ .../Graphics/include/Context.hpp | 1 + .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 17 ++++++- .../libSceAgcDriver/Graphics/src/State.cpp | 12 ++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 22 ++++++++ .../tests/execution/PixelSampleCoverage.cpp | 50 +++++++++++++++++-- 9 files changed, 125 insertions(+), 8 deletions(-) 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 9ef82e2572..a1af0b1806 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -262,7 +262,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; - if (s.samples != t.samples || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index cb96f0df18..9a2b217c63 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -352,6 +352,7 @@ struct VulkanDevice::State { bool textureCompressionBC = false; bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; + VkSampleCountFlags mergedCoverageSampleCounts = 0; bool fragmentShaderPixelInterlock = false; bool conservativeRasterization = false; bool provokingVertexLast = false; @@ -1440,6 +1441,27 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } nonSeamlessCubeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT}; nonSeamlessCubeFeatures.nonSeamlessCubeMap = VK_TRUE; + VkPhysicalDeviceCoverageReductionModeFeaturesNV coverageReductionFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV}; + if (hasExtension(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME) && hasExtension(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &coverageReductionFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + if (coverageReductionFeatures.coverageReductionMode == VK_TRUE) { + const auto combinations = state->InstanceFunction("vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); + std::uint32_t count = 0; + check(combinations(selected, &count, nullptr), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); + std::vector supported(count, {VK_STRUCTURE_TYPE_FRAMEBUFFER_MIXED_SAMPLES_COMBINATION_NV}); + check(combinations(selected, &count, supported.data()), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); + for (const auto& combination : supported) { + if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_MERGE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == 0) state->mergedCoverageSampleCounts |= combination.rasterizationSamples; + } + } + if (state->mergedCoverageSampleCounts != 0) { + deviceExtensions.push_back(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME); + deviceExtensions.push_back(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME); + } + } + coverageReductionFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV}; + coverageReductionFeatures.coverageReductionMode = VK_TRUE; VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT libraryFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT}; VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT}; VkPhysicalDeviceDynamicRenderingFeaturesKHR renderingFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR}; @@ -1707,6 +1729,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : nonSeamlessCubeFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &nonSeamlessCubeFeatures; } + if (state->mergedCoverageSampleCounts != 0) { + coverageReductionFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &coverageReductionFeatures; + } byteFeatures.pNext = const_cast(deviceInfo.pNext); if (state->fragmentShaderBarycentric) { barycentricFeatures.pNext = byteFeatures.pNext; @@ -3625,6 +3651,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.depthBiasClamp = state->depthBiasClamp; context.samplerFilterMinmax = state->samplerFilterMinmax; context.nonSeamlessCubeMap = state->nonSeamlessCubeMap; + context.mergedCoverageSampleCounts = state->mergedCoverageSampleCounts; context.conservativeRasterization = state->conservativeRasterization; context.meshWave32 = state->meshWave32; context.provokingVertexLast = state->provokingVertexLast; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 21a08c9361..eb28fe2922 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -193,6 +193,7 @@ struct Context { bool dualSrcBlend = false; bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; + VkSampleCountFlags mergedCoverageSampleCounts = 0; bool conservativeRasterization = false; bool meshWave32 = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 24af65b65e..465b9a0f45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -128,6 +128,7 @@ struct State { bool alphaToCoverage = false; bool ditheredAlphaToCoverage = false; bool perSampleShading = false; + bool mergedCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index a0198a33e8..4193628db4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -51,6 +51,7 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.renderExtent.width); mix(state.renderExtent.height); mix(state.samples); + mix(state.mergedCoverage); mix(state.customSampleLocations); if (state.customSampleLocations) { mix(state.sampleLocationGrid); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index de9d1904d6..adec5277e9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -242,6 +242,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.fragmentOutput, state.samples); appendKey(keys.fragmentShader, state.perSampleShading); appendKey(keys.fragmentOutput, state.perSampleShading); + appendKey(keys.fragmentShader, state.mergedCoverage); + appendKey(keys.fragmentOutput, state.mergedCoverage); const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); appendKey(keys.fragmentShader, sampleLocationsEnabled); appendKey(keys.fragmentOutput, sampleLocationsEnabled); @@ -314,6 +316,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(state.reversedBlend == 0 || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "blending into a color target with a reversed component order through a second source color needs dual-source blending, which the device does not support"); Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); + Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); @@ -379,7 +382,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = static_cast(state.samples); + color.samples = state.mergedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -412,6 +415,11 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput VkPipelineRenderingCreateInfoKHR rendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; rendering.colorAttachmentCount = static_cast(renderingColors.size()); rendering.pColorAttachmentFormats = renderingColors.empty() ? nullptr : renderingColors.data(); + const std::vector mergedColorSamples(renderingColors.size(), VK_SAMPLE_COUNT_1_BIT); + VkAttachmentSampleCountInfoNV attachmentSamples{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV}; + attachmentSamples.colorAttachmentCount = static_cast(mergedColorSamples.size()); + attachmentSamples.pColorAttachmentSamples = mergedColorSamples.empty() ? nullptr : mergedColorSamples.data(); + if (state.mergedCoverage) rendering.pNext = &attachmentSamples; if (state.depth) { rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format); if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat; @@ -482,6 +490,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const auto grid = SampleLocationGrid(state); sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, grid, state.samples * grid.width * grid.height, locations.data()}; if (sampleLocationsEnabled) samples.pNext = &sampleLocations; + VkPipelineCoverageReductionStateCreateInfoNV coverageReduction{VK_STRUCTURE_TYPE_PIPELINE_COVERAGE_REDUCTION_STATE_CREATE_INFO_NV}; + coverageReduction.coverageReductionMode = VK_COVERAGE_REDUCTION_MODE_MERGE_NV; + if (state.mergedCoverage) { + coverageReduction.pNext = samples.pNext; + samples.pNext = &coverageReduction; + } VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -851,6 +865,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); append(key, state.perSampleShading); + append(key, state.mergedCoverage); append(key, state.customSampleLocations); if (state.customSampleLocations) { append(key, state.sampleLocationGrid); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 47e4933079..3c8b5e0955 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -463,6 +463,12 @@ constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; constexpr std::uint32_t SampleZeroRefusedInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); +bool overrasterized(const Registers& cx, std::uint32_t samples) { + const auto eqaa = find(cx, 0x201); + const auto mode = find(cx, 0x292); + return samples > 1u && eqaa != cx.end() && mode != cx.end() && (mode->second & 1u) != 0 && ((eqaa->second >> 24u) & 7u) == static_cast(std::countr_zero(samples)); +} + bool sampleCoverageSupported(const QueueState& queue, std::uint32_t samples, bool perSampleShading) { if (samples == 1u || PixelProgramSkipped(queue)) return true; const auto ena = find(queue.context, 0x1b3); @@ -1253,8 +1259,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.customSampleLocations = true; result.sampleLocations.fill(0); } - if (color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); - Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); + const bool merged = color.samples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples); + if (!merged && color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); + Require((color.samples == result.samples && !result.mergedCoverage) || merged, "a color target's sample count differs from the rasterizer's"); + result.mergedCoverage = merged; APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { result.renderExtent = color.extent; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 12aa55ff64..f085f62b93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3162,6 +3162,28 @@ void multisampleTests() { } queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; + { + auto smooth = multisampled(); + smooth.context[0x2f8] = 0x0020c002u; + smooth.context[0x31d] = 0u; + smooth.context[0x201] = 0x02130000u; + smooth.context[0x1b3] = 0x4002u; + smooth.context[0x1b4] = 0x4002u; + const auto merged = DecodeState(smooth); + Require(merged.samples == 4 && merged.mergedCoverage && merged.colors.size() == 1 && merged.colors[0].samples == 1 && !merged.perSampleShading, "a radeonsi smoothing draw (4 raster samples, OVERRASTERIZATION_AMOUNT 2, one-sample target) did not decode as merged coverage"); + smooth.context[0x201] = 0x01130000u; + expectFailure([&] { DecodeState(smooth); }, "a color target's sample count differs from the rasterizer's"); + smooth.context[0x201] = 0x02130000u; + smooth.context[0x293] = 0x10000u; + expectFailure([&] { DecodeState(smooth); }, "SAMPLE_COVERAGE in a multisampled pixel shader"); + smooth.context[0x1b3] = 2u; + smooth.context[0x1b4] = 2u; + const auto iteratedSmooth = DecodeState(smooth); + Require(iteratedSmooth.samples == 1 && !iteratedSmooth.mergedCoverage && !iteratedSmooth.perSampleShading, "an overrasterized draw that runs per sample into a one-sample target did not rasterize sample 0 alone"); + smooth.context[0x293] = 0u; + smooth.context[0x292] = 2u; + expectFailure([&] { DecodeState(smooth); }, "a color target's sample count differs from the rasterizer's"); + } queue.context[0x302] = 0x44cc; state = DecodeState(queue); Require(state.customSampleLocations && state.sampleLocationGrid, "a standard 2x pattern with its samples swapped in one pixel did not decode as a grid"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp index 82924510e8..7c6d5398c2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include namespace { @@ -49,7 +50,11 @@ constexpr 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) std::array, 3> Programs{}; +constexpr std::array, 3> HalfTriangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 4> Programs{}; std::size_t programCount = 0; std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -145,11 +150,11 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t return color; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode, std::span, 3> triangle = Triangle, std::uint32_t rasterSamples = 1) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); - const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + 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{ @@ -201,7 +206,9 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge state.blends = {blend}; state.blend = blend; state.blendConstants = {}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + state.samples = rasterSamples; + state.mergedCoverage = rasterSamples > 1; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangle.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); } @@ -232,6 +239,40 @@ void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint3 } } +bool ExpectMerged(AgcDriver::VulkanDevice& device) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + try { + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), HalfTriangle, 4u); + } catch (const std::exception& error) { + if (std::string_view(error.what()).find("VK_NV_framebuffer_mixed_samples") == std::string_view::npos) throw; + return false; + } + const auto stored = ReadBack(device, block.Address(), bytes); + std::uint32_t full = 0; + std::uint32_t partial = 0; + std::uint32_t untouched = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++untouched; + continue; + } + std::array value{}; + std::memcpy(value.data(), texel, sizeof(value)); + const auto mask = static_cast(value[0]); + Require(value[0] == static_cast(mask) && mask >= 1u && mask <= 15u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "merged coverage: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + ")"); + if (mask == 15u) ++full; + else ++partial; + } + } + Require(full != 0 && partial != 0 && untouched != 0, "merged coverage: a half-covering triangle wrote " + std::to_string(full) + " fully covered, " + std::to_string(partial) + " partly covered and left " + std::to_string(untouched) + " pixels"); + return true; +} + } int main() { @@ -243,6 +284,7 @@ int main() { Expect(*device, PixelInputBit(PixelInput::SampleCoverage), 0u, "SAMPLE_COVERAGE alone"); Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center"); Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY"); + if (!ExpectMerged(*device)) std::puts("merged coverage not tested: the device lacks VK_NV_framebuffer_mixed_samples with merged coverage reduction"); std::puts("pixel sample coverage tests passed"); return 0; } catch (const std::exception& error) { From 51ac7d36e23a2d6a42bb484da9d4676762ed7dd4 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 10:11:54 +0000 Subject: [PATCH 3/4] fix(agc): journal merged coverage with the pipeline state Pipeline creation and the pipeline store key read State::mergedCoverage, but the journal's state codec and CanonicalPipelineState dropped it, so a journalled merged-coverage pipeline replayed as a plain multisampled one and the draw built its pipeline again. The codec and the canonical state now carry it and the journal format becomes 4. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 77d4566132..34191214ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 3u; +constexpr std::uint32_t FileFormat = 4u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -215,6 +215,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.customSampleLocations); writer.Bool(state.sampleLocationGrid); writer.Bool(state.perSampleShading); + writer.Bool(state.mergedCoverage); for (const auto mask : state.quadSampleMask) writer.Raw(mask); } @@ -273,6 +274,7 @@ void decodeState(Reader& reader, State& state) { state.customSampleLocations = reader.Bool(); state.sampleLocationGrid = reader.Bool(); state.perSampleShading = reader.Bool(); + state.mergedCoverage = reader.Bool(); for (auto& mask : state.quadSampleMask) mask = reader.Raw(); } @@ -1036,6 +1038,7 @@ State CanonicalPipelineState(const State& state) { canonical.customSampleLocations = state.customSampleLocations; canonical.sampleLocationGrid = state.sampleLocationGrid; canonical.perSampleShading = state.perSampleShading; + canonical.mergedCoverage = state.mergedCoverage; canonical.quadSampleMask = state.quadSampleMask; return canonical; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f085f62b93..70032f6de6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9104,6 +9104,15 @@ void pipelineJournalTests() { BuildPipelineStoreKey(masked, context, maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); BuildPipelineStoreKey(maskReplayed, context, maskDecoded.state, maskDecoded.vertexInput, layouts.View(), shaders, maskDecoded.attachmentLayout); Require(masked != fourSamples && masked == maskReplayed, "the decoded sample mask state keys another pipeline"); + auto mergedState = fourSampleState; + mergedState.mergedCoverage = true; + GraphicsPipelineRecipe mergedDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), mergedDecoded), "the merged coverage recipe does not decode"); + Require(mergedDecoded.state.mergedCoverage, "the recipe dropped merged coverage"); + std::vector mergedKey, mergedReplayed; + BuildPipelineStoreKey(mergedKey, context, mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(mergedReplayed, context, mergedDecoded.state, mergedDecoded.vertexInput, layouts.View(), shaders, mergedDecoded.attachmentLayout); + Require(mergedKey != fourSamples && mergedKey == mergedReplayed, "the decoded merged coverage state keys another pipeline"); auto movedState = state; movedState.viewport.x = 5; movedState.colors[0].address = 0x9000; From f0b208a0734404e4e48b0075c12cacfc64cc6eeb Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 19:06:44 +0000 Subject: [PATCH 4/4] test(agc): expect one-sample rasterization of a smoothing draw without MSAA_ENABLE Since #6196, a one-sample target under a multisampled rasterizer with MSAA_ENABLE clear is drawn as one sample instead of refused, so the overrasterized draw with MSAA_ENABLE clear now decodes as one sample without merged coverage. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 70032f6de6..523bf17e51 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3182,7 +3182,8 @@ void multisampleTests() { Require(iteratedSmooth.samples == 1 && !iteratedSmooth.mergedCoverage && !iteratedSmooth.perSampleShading, "an overrasterized draw that runs per sample into a one-sample target did not rasterize sample 0 alone"); smooth.context[0x293] = 0u; smooth.context[0x292] = 2u; - expectFailure([&] { DecodeState(smooth); }, "a color target's sample count differs from the rasterizer's"); + const auto unmerged = DecodeState(smooth); + Require(unmerged.samples == 1 && !unmerged.mergedCoverage, "an overrasterized draw into a one-sample target without MSAA_ENABLE did not rasterize as one sample"); } queue.context[0x302] = 0x44cc; state = DecodeState(queue);