Merge pull request #3228 from GustavoGamarra95/feat/agc-alpha-to-coverage

feat(agc): emulate undithered alpha-to-coverage
This commit is contained in:
Adrià 2026-10-11 09:22:23 +02:00 committed by GitHub
commit be8a67db1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 411 additions and 8 deletions

View File

@ -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_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)

View File

@ -114,6 +114,7 @@ std::shared_ptr<DrawDecode> 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;
}

View File

@ -68,6 +68,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
for (const auto value : pixel.targetExportMapping) mix(value);
for (const auto value : pixel.targetExportPacking) mix(static_cast<std::uint64_t>(value));
mix(pixel.dualSourceBlend);
mix(pixel.alphaToCoverage);
std::lock_guard cacheLock(drawCacheMutex);
++drawEntryCounters.lookups;
const auto found = drawCache.find(drawKey);

View File

@ -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;
}

View File

@ -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;

View File

@ -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");
@ -773,6 +775,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<unsigned>(result.topology));
return result;
}
@ -986,7 +995,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
std::uint32_t zFormat = 0;
static_cast<void>(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;

View File

@ -1529,6 +1529,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;
@ -1546,7 +1566,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();
@ -4055,6 +4075,7 @@ int main() {
ComputeScratchTests();
shaderUserDataTailPaddingTests();
InitialContextTests();
AlphaToCoverageTests();
pushConstantTests();
resourceTests();
descriptorSnapshotTests();

View File

@ -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<std::uint32_t, 1> code{0xbf810000u};
const std::array<std::uint32_t, 3> 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<std::array<std::uint8_t, 8>, 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;
@ -1375,8 +1380,8 @@ void verifyPixelRequestSerialization() {
const auto encoded = serializer.Serialize(minimal);
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<void>(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<void>(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=", "NVNQQREAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
@ -1467,6 +1472,7 @@ void verifyLegacyPixelRequests() {
require(pixel.targetExportMapping == std::array<std::uint8_t, 8>{}, "legacy pixel mapping no longer defaults to zero");
require(pixel.targetExportPacking == std::array<ColorExportPacking, 8>{}, "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");

View File

@ -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 <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::ColorTargetLayout;
using AgcDriver::Graphics::ColorTileMode;
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
using Color = std::array<float, 4>;
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<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
constexpr std::array<std::array<float, 4>, 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<Color, 4> 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<std::array<std::uint32_t, 64>, 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<std::uint32_t>(value);
Require((bits & 0x1fffu) == 0, "the test's half values are exact");
return static_cast<std::uint16_t>(((((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<float>(mantissa), -24) : std::ldexp(1.0f + static_cast<float>(mantissa) / 1024.0f, static_cast<int>(exponent) - 15);
}
std::span<const std::uint32_t> 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<std::uint32_t>(color[channel]));
code[at++] = 0xf800180fu;
code[at++] = 0x07060504u;
}
code[at++] = 0xbf810000u;
return std::span<const std::uint32_t>(code.data(), at);
}
struct Block {
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
#ifdef _WIN32
if (watched) {
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
} else {
data = static_cast<std::uint8_t*>(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<std::uintptr_t>(raw);
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
data = reinterpret_cast<std::uint8_t*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
} else {
data = static_cast<std::uint8_t*>(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<std::uintptr_t>(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<std::uint32_t>(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<std::uint32_t>(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<const std::uint32_t> pixelCode) {
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto address = reinterpret_cast<std::uintptr_t>(Triangle.data());
const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Triangle.size()), 0x01016facu};
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(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<float>(Height), static_cast<float>(Width), -static_cast<float>(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<std::uintptr_t>(pixelCode.data());
const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}};
const std::array<ShaderRecompiler::MemoryRegion, 1> 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<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
}
std::vector<std::uint16_t> ReadBack(AgcDriver::VulkanDevice& device, const Block& block) {
AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back");
device.WaitIdle();
std::vector<std::uint16_t> stored(block.bytes / 2u);
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1);
return stored;
}
std::vector<std::uint16_t> Fill(Block& block, const ColorTargetLayout& layout) {
std::vector<std::uint16_t> 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<std::uint16_t>& before, const std::function<Color(const Color&)>& 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<Case, 17> 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<float>(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<float>(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;
}
}

View File

@ -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);
}

View File

@ -324,6 +324,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
writer.WriteU8(static_cast<std::uint8_t>(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) |

View File

@ -173,6 +173,7 @@ struct ShaderPixelInputInfo {
bool psExecuteOnNoop = false;
ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ;
bool psOrderedPixelShader = false;
bool psAlphaToCoverage = false;
ShaderStageRuntime stage;
bool HasPositionInput() const {

View File

@ -140,6 +140,7 @@ struct ShaderPixelStageInfo {
std::array<std::uint8_t, 8> targetExportMapping;
std::array<ColorExportPacking, 8> targetExportPacking;
bool dualSourceBlend;
bool alphaToCoverage;
};
struct ShaderVertexBufferResource {

View File

@ -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) {

View File

@ -180,7 +180,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) {

View File

@ -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);
@ -992,6 +1012,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);
}
@ -1244,6 +1265,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);
}

View File

@ -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;

View File

@ -121,6 +121,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