From d152ff833c796d9dd742d2c084f7b8dc3054837d Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:07:32 -0300 Subject: [PATCH 1/3] feat(shader): turn the MRT0 alpha into a sample mask for alpha-to-coverage A pixel shader whose stage info asks for alpha-to-coverage declares SampleMask, starts with every sample covered and, at its MRT0 export, clears the mask when the alpha is below one half or NaN, running with late fragment tests. The shader keeps running, as a zero coverage mask does on the GPU. The flag is part of the static shader identity and of the request serialization, now version 16. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 16 +++++++++----- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +++-- .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 4 ++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 22 +++++++++++++++++++ .../src/ShaderInputInfoBuilder.cpp | 1 + 9 files changed, 46 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c1c8977ac..7ddeec523 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1295,7 +1295,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.targetOutputMode, value.targetExportMapping, value.alphaToCoverage); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1338,6 +1338,7 @@ void verifyPixelRequestSerialization() { .targetOutputMode = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u} }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; + pixel.alphaToCoverage = true; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1362,6 +1363,10 @@ void verifyPixelRequestSerialization() { RecompileCacheKey::Build(changed, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(changed), "runtime pixel export mapping changed the static shader identity"); } + auto withoutCoverage = request; + 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"); } request.context.pixel.reset(); request.shader.stage = ShaderStage::Vertex; @@ -1373,12 +1378,12 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQQ8AAAA=", "new requests did not use serialization version 15"); + require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 17u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing or dual-source flag was accepted"); + for (std::size_t bytes = 0; bytes < 18u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source or alpha-to-coverage flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRAAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1467,6 +1472,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); 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.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/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 4207ad6ab..ddffedd3c 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -176,6 +176,7 @@ private: append(key, value.executeOnNoop); append(key, value.conservativeZExport); append(key, value.orderedPixelShader); + append(key, value.alphaToCoverage); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index e5b8a2f7c..60ad4c460 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -324,6 +324,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(value)); } writer.WriteBool(info.dualSourceBlend); + writer.WriteBool(info.alphaToCoverage); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -371,6 +372,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 < 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) | @@ -732,7 +734,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(15u); + writer.WriteU32(16u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -759,7 +761,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 > 15u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 16u) 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 8f72d9821..45f4984f5 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -173,6 +173,7 @@ struct ShaderPixelInputInfo { bool psExecuteOnNoop = false; ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; bool psOrderedPixelShader = false; + bool psAlphaToCoverage = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5688af8e8..951101c82 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -140,6 +140,7 @@ struct ShaderPixelStageInfo { std::array targetExportMapping; std::array targetExportPacking; bool dualSourceBlend; + bool alphaToCoverage; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 79c762646..daf4a4110 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -355,6 +355,10 @@ void DefineOutputs(SpirvEmitterState& state) { } } } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage) { + if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported"); + BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); + } } void DefineDescriptors(SpirvEmitterState& state) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index b8d68e795..26e0c761a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -176,7 +176,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) { + if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !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 07f3ca948..45145cfc6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -272,6 +272,26 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target return result; } +bool AlphaToCoverage(const SpirvEmitterState& state) { + return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage; +} + +void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpStore, pointer, mask); +} + +void StoreAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + const auto alpha = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); + const auto below = state.module.AllocateId(); + state.module.AddFunction(spv::OpFUnordLessThan, TypeBool(state), below, alpha, ConstantF32(state, 0x3f000000u)); + const auto mask = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeI32(state), mask, below, ConstantI32(state, 0), ConstantI32(state, -1)); + StoreCoverage(state, mask); +} + std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vector, std::uint32_t component) { const auto value = ctx.state.module.AllocateId(); ctx.state.module.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), value, vector, component); @@ -983,6 +1003,7 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } + if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } @@ -1235,6 +1256,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto mapped = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vectorType, mapped, components[0], components[1], components[2], components[3]); value = mapped; + if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); const auto mode = MrtOutputMode(state, exp); if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index c14e244be..18d89e989 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -141,6 +141,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; pixelStorage.psConservativeZExport = pixel.conservativeZExport; pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader; + pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; From 7d92bf70f9487adbe7bc1e7eeae664ad18a830aa Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:07:40 -0300 Subject: [PATCH 2/3] feat(agc): decode undithered alpha-to-coverage DB_ALPHA_TO_MASK 0xaa01, the enable bit with the four offsets at 2 and no OFFSET_ROUND, reaches the pixel stage, which keeps a pixel when its MRT0 alpha is at least one half. Dithered or rounded settings, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha, still throw. God of War Sons of Sparta (PPSA28997) stopped on it a few minutes into play. --- .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 15 ++++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 23 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index f4444a808..b784526d6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -114,6 +114,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; + product->pixel.alphaToCoverage = product->state.alphaToCoverage; return product; } 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 090b9b10d..14090d411 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -68,6 +68,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(value)); mix(pixel.dualSourceBlend); + mix(pixel.alphaToCoverage); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); 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 1759f417e..3e1fd6a77 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -105,7 +105,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; 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) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 05cb27663..df0a89c64 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -100,6 +100,7 @@ struct State { VkViewport viewport; bool negativeOneToOne; bool depthClamp = false; + bool alphaToCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index f29320922..f5818214d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -104,6 +104,7 @@ constexpr std::uint32_t DepthControlMask = ~0x007007f0u; constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u); constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; +constexpr std::uint32_t AlphaToCoverageUndithered = 0x0000aa01u; constexpr std::uint32_t ScanModeMask = ~0x22u; constexpr std::uint32_t ScanControlMask = ~0x76023fffu; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; @@ -597,7 +598,8 @@ State DecodeState(const QueueState& queue) { Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); } zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); - zero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); + result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; + if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x292, ScanModeMask, "scan conversion mode"); zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); @@ -770,6 +772,13 @@ State DecodeState(const QueueState& queue) { Require(((exportFormat >> 4u) & 0xfu) == (exportFormat & 0xfu), "dual-source blending needs the MRT1 export in the MRT0 export format"); Require(((shaderMask >> 4u) & 0xfu) == (shaderMask & 0xfu), "dual-source blending needs the MRT1 export with the MRT0 shader mask"); } + if (result.alphaToCoverage) { + const auto mrt0 = std::find_if(result.colors.begin(), result.colors.end(), [](const ColorTarget& color) { return color.exportIndex == 0; }); + Require(mrt0 != result.colors.end(), "alpha-to-coverage without a written MRT0 color target is unsupported"); + const auto format = exportFormat & 0xfu; + Require(format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); + Require(mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None, "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); + } APS5_LOG_OUT_DEBUG("DecodeState done colorTarget=%u render=%ux%u topology=%u", result.hasColorTarget ? 1u : 0u, result.renderExtent.width, result.renderExtent.height, static_cast(result.topology)); return result; } @@ -983,7 +992,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t zFormat = 0; static_cast(value(cx, 0x1c4, zFormat)); if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); !reason.empty()) return reason; + if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) { + if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; + } if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cf7a795c0..52aff8942 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1516,6 +1516,26 @@ void DepthClipTests() { } } +void AlphaToCoverageTests() { + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(!AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage decoded while disabled"); + queue.context[0x2dc] = 0xaa01; + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected undithered alpha-to-coverage"); + for (const auto control : {0x8701u, 0x1aa01u, 0xaa03u}) { + queue.context[0x2dc] = control; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage"); + } + queue.context[0x2dc] = 0xaa01; + const auto exportFormat = queue.context.at(0x1c5); + queue.context[0x1c5] = (exportFormat & ~0xfu) | 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); + queue.context[0x1c5] = exportFormat; +} + void InitialContextTests() { const auto configured = makeState(); AgcDriver::QueueState queue; @@ -1533,7 +1553,7 @@ void InitialContextTests() { } queue.context[0x206] = 0x43f; queue.context[0x2dc] |= 1; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); queue.context.erase(0x2dc); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); queue.ClearContext(); @@ -4041,6 +4061,7 @@ int main() { ComputeScratchTests(); shaderUserDataTailPaddingTests(); InitialContextTests(); + AlphaToCoverageTests(); pushConstantTests(); resourceTests(); descriptorSnapshotTests(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 237d82bed..df0e2020f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -117,6 +117,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceVdecsw](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown - [sceGameLiveStreamingGetCurrentStatus2](../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id - [Depth clear draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured +- [Alpha-to-coverage](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw - [GPU flips](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceAgcDriverGetEqContextId](../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted From 50e84c7cf77fd1badbcb336462e18493b40bb2ca Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:07:40 -0300 Subject: [PATCH 3/3] test(agc): pin alpha-to-coverage coverage measured on RDNA2 The expected rows were measured with Vulkan alpha-to-coverage on a Radeon 660M (gfx1035) under RADV_DEBUG=noatocdithering, which programs DB_ALPHA_TO_MASK 0xaa01: an alpha of at least one half, 2 and +inf keep the pixel; below one half, -0, negatives, -inf and NaN drop it. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../tests/execution/AlphaToCoverage.cpp | 327 ++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 866503983..4d7fb8317 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -905,6 +905,8 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) + target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $) target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp new file mode 100644 index 000000000..5fa517874 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -0,0 +1,327 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.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 "Optimization/ResourceProgram.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 +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +using Color = std::array; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +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} +}}; + +constexpr std::array Destinations{{ + {0.5f, 0.25f, 0.75f, 0.5f}, {1.0f, 0.0f, 0.125f, 0.25f}, {0.0625f, 0.875f, 0.5f, 1.0f}, {0.25f, 0.5f, 0.375f, 0.75f} +}}; + +alignas(256) std::array, 8> Programs{}; +std::size_t programCount = 0; + +std::uint16_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return static_cast(((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu)); +} + +float FromHalf(std::uint16_t half) { + const auto exponent = (half >> 10u) & 0x1fu; + const auto mantissa = half & 0x3ffu; + Require((half & 0x8000u) == 0 && exponent != 31u, "a blended texel is negative, infinite or NaN"); + return exponent == 0 ? std::ldexp(static_cast(mantissa), -24) : std::ldexp(1.0f + static_cast(mantissa) / 1024.0f, static_cast(exponent) - 15); +} + +std::span Program(const Color& color, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + const auto move = [&](std::uint32_t vgpr, std::uint32_t value) { + code[at++] = 0x7e0002ffu | (vgpr << 17u); + code[at++] = value; + }; + if (compressed) { + move(4, Half(color[0]) | (Half(color[1]) << 16u)); + move(5, Half(color[2]) | (Half(color[3]) << 16u)); + code[at++] = 0xf8001c0fu; + code[at++] = 0x00000504u; + } else { + for (std::uint32_t channel = 0; channel < 4; ++channel) move(4 + channel, std::bit_cast(color[channel])); + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + } + 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) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float16x4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.bytes == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool alphaToCoverage, std::uint32_t exportFormat, std::span pixelCode) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + 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()); + + 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}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.alphaToCoverage = alphaToCoverage; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + auto prepared = AgcDriver::Graphics::DecodePixelStageInfo(context, {}); + prepared.alphaToCoverage = alphaToCoverage; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, prepared, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->alphaToCoverage = state.alphaToCoverage; + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + 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, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block, const ColorTargetLayout& layout) { + std::vector halves(block.bytes / 2u); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto& color = Destinations[(x + y) % Destinations.size()]; + const auto index = layout.Offset(x, y) / 2u; + for (std::uint32_t channel = 0; channel < 4; ++channel) halves[index + channel] = Half(color[channel]); + } + } + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, const std::function& expectedColor, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const auto expected = expectedColor(Destinations[(x + y) % Destinations.size()]); + for (std::uint32_t channel = 0; channel < 3; ++channel) { + const bool inside = x < ScissorWidth; + const bool match = inside ? std::fabs(FromHalf(stored[index + channel]) - expected[channel]) <= 1.0f / 2048.0f : stored[index + channel] == before[index + channel]; + if (match) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) %s the scissor holds half 0x%04x, expected %.6g", channel, x, y, inside ? "inside" : "outside", stored[index + channel], inside ? expected[channel] : FromHalf(before[index + channel])); + throw std::runtime_error(what + detail); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + Block block(layout.Bytes()); + struct Case { + std::uint32_t alpha; + bool alphaToCoverage; + bool written; + }; + constexpr std::array cases{{ + {0x00000000u, true, false}, {0x80000000u, true, false}, {0x3e800000u, true, false}, {0x3efffffeu, true, false}, + {0x3effffffu, true, false}, {0x3f000000u, true, true}, {0x3f000001u, true, true}, {0x3f000002u, true, true}, + {0x3f400000u, true, true}, {0x3f800000u, true, true}, {0x40000000u, true, true}, {0xbf800000u, true, false}, + {0x7f800000u, true, true}, {0xff800000u, true, false}, {0x7fc00000u, true, false}, {0xffc00000u, true, false}, + {0x3e800000u, false, true}, + }}; + for (const auto& test : cases) { + const Color source{0.25f, 0.5f, 0.125f, std::bit_cast(test.alpha)}; + const auto before = Fill(block, layout); + Draw(*device, DecodeTarget(block), test.alphaToCoverage, Float32Export, Program(source, false)); + char name[96]; + std::snprintf(name, sizeof(name), "alpha 0x%08x with alpha-to-coverage %s", test.alpha, test.alphaToCoverage ? "on" : "off"); + Check(*device, block, layout, before, [&](const Color& destination) { return test.written ? source : destination; }, name); + programCount = 0; + } + for (const Case test : {Case{0x3f400000u, true, true}, Case{0x3e800000u, true, false}}) { + const Color source{0.25f, 0.5f, 0.125f, std::bit_cast(test.alpha)}; + const auto before = Fill(block, layout); + Draw(*device, DecodeTarget(block), true, Float16Export, Program(source, true)); + Check(*device, block, layout, before, [&](const Color& destination) { return test.written ? source : destination; }, test.written ? "alpha 0.75 with FP16_ABGR exports" : "alpha 0.25 with FP16_ABGR exports"); + programCount = 0; + } + std::puts("alpha-to-coverage tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}