fix(agc): draw a one-fragment multisampled color target as one sample with sample 0's coverage

A color target whose CB_COLOR_ATTRIB has more samples than fragments (4 samples, 1 fragment) was refused
as EQAA. With one fragment, a write is kept only where sample 0 is covered, which is the truncated
coverage reduction one-sample targets beside a multisampled depth target already use. Decode such a
target as one sample, keep its coverage sample count, and take that path. Targets with more than one
but fewer fragments than samples still throw.

The host does not write the FMASK of these targets, so a guest FMASK read of one that was drawn on the
host throws.
This commit is contained in:
fma-rc committed 2026-10-24 16:08:40 +02:00
1 parent 5ed5427bac
commit b76ab1f6c0
9 files changed
+145 -13

No files matched your search

@@ -98,6 +98,7 @@ float EffectiveMinLod(const GuestTextureResource& resource);
bool IsNullTextureResource(std::span<const std::uint32_t> words);
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words);
std::array<std::uint32_t, 8> EncodeTextureResource(const GuestTextureResource& resource);
void NoteOneFragmentFmask(std::uint64_t address);
bool DescriptorSingleLevel(std::span<const std::uint32_t> words);
bool FirstLayerDescriptor(std::span<const std::uint32_t> words, std::array<std::uint32_t, 8>& narrowed);
bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension);
@@ -67,6 +67,7 @@ struct ColorTarget {
bool uintExport = false;
std::uint32_t pipeBankXor = 0;
std::uint32_t samples = 1;
std::uint32_t coverageSamples = 1;
std::uint32_t dccControl = 0;
std::uint64_t fmaskAddress = 0;
std::uint32_t fmaskPipeBankXor = 0;
@@ -292,8 +293,8 @@ inline constexpr std::array<DrawKeyRange, 50> DrawKeyRegisters{{
// DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE,
// PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL.
{RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2af, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1},
// CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3.
{RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18},
// CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, CMASK_BASE_EXT, FMASK_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3.
{RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 0x10}, {RegisterBank::Context, 0x3a8, 0x18},
// The pixel program address, RSRC2 and user words; the geometry-back user pointer and program
// address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address;
// the hull user pointer, program address, RSRC2 and user words; the local program address.
@@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp"
@@ -2519,6 +2520,7 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara
const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes);
static_cast<void>(FlushDepthPlanes(color.address, colorLayout.Bytes(), DepthPlaneScope::Whole));
RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes());
if (color.fmaskAddress != 0) NoteOneFragmentFmask(color.fmaskAddress);
constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
timer.phase(PhaseSetup);
// Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again.
@@ -3233,6 +3235,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
targets.reserve(recipe.targets.size());
for (std::size_t index = 0; index < recipe.targets.size(); ++index) {
timer.phase(PhaseSetup);
if (state.colors[index].fmaskAddress != 0) NoteOneFragmentFmask(state.colors[index].fmaskAddress);
auto stored = recipe.targets[index].lock();
if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone);
auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] {
@@ -8,6 +8,8 @@
#include <array>
#include <bit>
#include <optional>
#include <mutex>
#include <unordered_set>
#include <stdexcept>
#include <string>
@@ -15,6 +17,21 @@ namespace AgcDriver::Graphics {
namespace {
std::mutex& oneFragmentFmaskMutex() {
static std::mutex mutex;
return mutex;
}
std::unordered_set<std::uint64_t>& oneFragmentFmasks() {
static std::unordered_set<std::uint64_t> addresses;
return addresses;
}
bool oneFragmentFmask(std::uint64_t address) {
std::lock_guard lock(oneFragmentFmaskMutex());
return oneFragmentFmasks().contains(address & ~0xffffull);
}
void requireValidDstSel(std::uint32_t value) {
Require(value == 0 || value == 1 || (value >= 4 && value <= 7), "guest texture descriptor has an invalid destination channel selector");
}
@@ -186,6 +203,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata");
Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned");
Require(!metaCompress || format == static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK");
Require(!oneFragmentFmask(baseAddress), "reading the FMASK of a one-fragment color target drawn on the host is not implemented (the host does not write it)");
Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface");
const bool identity = format != static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8) && dstSelX == 4u && dstSelY == 5u && dstSelZ == 6u && dstSelW == 7u;
for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(identity || selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented");
@@ -369,4 +387,9 @@ bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextur
return view.dimension == TextureDimension::k2DArray && view.depthOrLastArray <= surface.depthOrLastArray;
}
void NoteOneFragmentFmask(std::uint64_t address) {
std::lock_guard lock(oneFragmentFmaskMutex());
oneFragmentFmasks().insert(address & ~0xffffull);
}
}
@@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update
static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec");
static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec");
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(State) == 824, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(State) == 832, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec");
@@ -1504,8 +1504,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
result.customSampleLocations = true;
result.sampleLocations.fill(0);
}
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);
const bool merged = color.samples == 1u && color.coverageSamples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples);
if (!merged && color.samples == 1 && color.coverageSamples == 1u && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result);
Require(color.coverageSamples == color.samples || color.coverageSamples == result.samples || (result.samples == 1u && (read(cx, 0x292) & 1u) == 0), "a one-fragment color target's sample count differs from the rasterizer's");
const bool truncated = color.samples == 1u && result.samples > 1u && result.depth && result.depth->samples == result.samples && (result.colors.empty() || result.truncatedCoverage) && !overrasterized(cx, result.samples);
Require((color.samples == result.samples && !result.mergedCoverage && !result.truncatedCoverage) || merged || truncated, "a color target's sample count differs from the rasterizer's");
result.mergedCoverage = merged;
@@ -1883,10 +1884,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
const auto decoded = DecodeColorFormat(format, number, swap);
const auto attrib = read(cx, 0x31d + stride);
zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes");
color.samples = 1u << ((attrib >> 12u) & 7u);
Require(color.samples <= 8u, "color targets with more than eight samples are unsupported");
Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported");
const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u;
color.coverageSamples = 1u << ((attrib >> 12u) & 7u);
Require(color.coverageSamples <= 8u, "color targets with more than eight samples are unsupported");
color.samples = 1u << ((attrib >> 15u) & 3u);
Require(color.samples == color.coverageSamples || color.samples == 1u, "color targets with more than one but fewer fragments than samples (EQAA) are unsupported");
const auto multisampleInfo = color.coverageSamples > 1 ? 0x0c006000u : 0u;
// ROUND_MODE (bit 18) only affects unorm rounding and packed float truncation. With DCC_ENABLE (bit 28) the target is written
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
Require((info & 0x8000u) != 0 || number <= 1u || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
@@ -2026,6 +2028,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
color.fmaskAddress = address - color.fmaskPipeBankXor;
}
}
if (color.coverageSamples > color.samples && (info & 0x4000u) != 0) {
const auto fmaskHigh = find(cx, 0x3a0 + slot);
color.fmaskAddress = ((fmaskHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(fmaskHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x321 + stride)) << 8u);
Require(color.fmaskAddress != 0, "a compressed one-fragment color target without an FMASK address is unsupported");
}
if ((info & 0x10000000u) != 0) {
if (maxMip == 0) {
const auto dccHigh = find(cx, 0x3a8 + slot);
@@ -3832,7 +3832,7 @@ void multisampleTests() {
}
queue = multisampled();
queue.context[0x31d] = 0x1000;
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
queue.context[0x31d] = 0;
queue.context[0x293] = 0x10000u;
Require(DecodeState(queue).samples == 1 && !DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample target did not rasterize sample 0 alone");
@@ -4328,6 +4328,42 @@ void multisampledDepthTests() {
queue = truncatedTwoSample();
queue.context[0x31d] = 0x12000;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
const auto oneFragment = [&] {
auto queue = singleSampleScan();
queue.context[0x2f8] = 0x00210002;
queue.context[0x292] = 3;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae;
queue.context[0x31d] = 0x2000;
queue.context[0x201] = 0x112202;
return queue;
};
queue = oneFragment();
state = DecodeState(queue);
Require(state.samples == 4 && !state.customSampleLocations && state.depth && state.depth->samples == 4 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.truncatedCoverage && !state.mergedCoverage && !state.perSampleShading, "a 4-sample color target with one fragment beside a 4x depth target (PPSA02015's registers) did not keep sample 0's color in its one fragment");
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a 4-sample color target with one fragment beside a 4x depth target");
queue = oneFragment();
queue.context[0x31c] |= 0x4000u;
queue.context[0x321] = 0x00c3a1b2u;
queue.context[0x3a0] = 0x01u;
state = DecodeState(queue);
Require(state.colors.size() == 1 && state.colors[0].fmaskAddress == ((1ull << 40u) | (0x00c3a1b2ull << 8u)), "a compressed one-fragment color target did not decode its FMASK address");
queue.context[0x321] = 0u;
queue.context[0x3a0] = 0u;
expectFailure([&] { DecodeState(queue); }, "a compressed one-fragment color target without an FMASK address is unsupported");
queue = oneFragment();
Require(DecodeState(queue).colors[0].fmaskAddress == 0u, "an uncompressed one-fragment color target noted an FMASK address");
queue = oneFragment();
queue.context[0x31d] = 0xa000;
expectFailure([&] { DecodeState(queue); }, "color targets with more than one but fewer fragments than samples (EQAA) are unsupported");
queue = oneFragment();
queue.context[0x2f8] = 0x00100001;
queue.context[0x010] = 0x22900987;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44;
expectFailure([&] { DecodeState(queue); }, "a one-fragment color target's sample count differs from the rasterizer's");
queue = oneFragment();
queue.context[0x010] = 0;
queue.context[0x011] = 0;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
queue = singleSampleScan();
queue.context[0x010] = 0x22900983;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
@@ -178,6 +178,21 @@ void RunGuestTextureResourceTests() {
rejectFields(compressedFmask, "FMASK CMASK metadata must be pipe aligned");
compressedFmask.metaPipeAligned = compressedFmask.writeCompress = true;
rejectFields(compressedFmask, "FMASK CMASK compression is not implemented");
Fields oneFragmentFmask = fmask;
oneFragmentFmask.base40 = 0x6540000ull;
static_cast<void>(DecodeTextureResource(pack(oneFragmentFmask)));
NoteOneFragmentFmask(oneFragmentFmask.base40 << 8u);
rejectFields(oneFragmentFmask, "reading the FMASK of a one-fragment color target drawn on the host is not implemented");
Fields xoredFmask = oneFragmentFmask;
xoredFmask.base40 |= 0xa5ull;
rejectFields(xoredFmask, "reading the FMASK of a one-fragment color target drawn on the host is not implemented");
Require(DecodeTextureResource(pack(fmask)).baseAddress == fmaskResource.baseAddress, "an FMASK read of another surface was refused after a one-fragment target noted its FMASK");
Fields oneFragmentColor = oneFragmentFmask;
oneFragmentColor.format = base.format;
oneFragmentColor.dstSelY = base.dstSelY;
oneFragmentColor.dstSelZ = base.dstSelZ;
oneFragmentColor.dstSelW = base.dstSelW;
Require(DecodeTextureResource(pack(oneFragmentColor)).baseAddress == (oneFragmentFmask.base40 << 8u), "a color read at a noted FMASK address was refused");
Fields fmaskArray = fmask;
fmaskArray.typeRaw = 13u;
fmaskArray.depth = 2u;
@@ -67,7 +67,7 @@ constexpr std::array<std::array<float, 4>, 3> OtherHalfTriangle{{
{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<std::array<std::uint32_t, 16>, 8> Programs{};
alignas(256) std::array<std::array<std::uint32_t, 16>, 16> Programs{};
alignas(256) std::array<std::byte, 256 * 1024> DepthMemory{};
std::size_t programCount = 0;
@@ -164,12 +164,12 @@ struct Block {
std::uint8_t* data = nullptr;
};
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) {
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info, std::uint32_t attrib = 0) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
cx[0x31b] = 0;
cx[0x31c] = info;
cx[0x31d] = 0;
cx[0x31d] = attrib;
cx[0x323] = 0;
cx[0x324] = 0;
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
@@ -376,6 +376,51 @@ std::string ExpectTruncated(AgcDriver::VulkanDevice& device) {
}
std::string ExpectOneFragment(AgcDriver::VulkanDevice& device) {
if ((AgcDriver::VulkanDeviceTestAccess::Context(device).limits.framebufferDepthSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) return "the device has no four-sample depth targets (framebufferDepthSampleCounts)";
constexpr std::size_t bytes = std::size_t{Width} * Height * 16u;
AgcDriver::Graphics::DepthTarget depth{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0};
depth.samples = 4;
std::array<std::uint32_t, 16> written{};
std::uint32_t untouched = 0;
for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) {
Block block(bytes);
std::fill(block.data, block.data + bytes, std::uint8_t{0xff});
const auto color = DecodeTarget(block, Rgba32FloatInfo, 0x2000u);
Require(color.samples == 1u, "a 4-sample color target with one fragment did not decode as one stored sample");
try {
Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), *triangle, 4u, &depth);
} catch (const std::exception& error) {
if (std::string_view(error.what()).find("truncated coverage reduction") == std::string_view::npos) throw;
return "the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction";
}
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) {
const auto* texel = stored.data() + (static_cast<std::size_t>(y) * Width + x) * 16u;
if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) {
++untouched;
continue;
}
std::array<float, 4> value{};
std::memcpy(value.data(), texel, sizeof(value));
const auto mask = static_cast<std::uint32_t>(value[0]);
Require(value[0] == static_cast<float>(mask) && mask >= 1u && mask <= 15u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "one-fragment 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]) + ")");
++written[mask];
}
}
}
std::uint32_t withoutSampleZero = 0;
std::uint32_t partial = 0;
for (std::uint32_t mask = 1; mask < 16u; ++mask) {
if ((mask & 1u) == 0) withoutSampleZero += written[mask];
else if (mask != 15u) partial += written[mask];
}
Require(withoutSampleZero == 0, "one-fragment coverage: " + std::to_string(withoutSampleZero) + " pixels whose coverage lacks sample 0 were written into the one fragment");
Require(written[15] != 0 && partial != 0 && untouched != 0, "one-fragment coverage: two half-covering triangles wrote " + std::to_string(written[15]) + " fully covered and " + std::to_string(partial) + " partly covered pixels and left " + std::to_string(untouched) + " untouched");
return {};
}
int main() {
try {
using ShaderRecompiler::PixelInput;
@@ -387,6 +432,7 @@ int main() {
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");
if (const auto skipped = ExpectTruncated(*device); !skipped.empty()) std::printf("truncated coverage not tested: %s\n", skipped.c_str());
if (const auto skipped = ExpectOneFragment(*device); !skipped.empty()) std::printf("one-fragment coverage not tested: %s\n", skipped.c_str());
std::puts("pixel sample coverage tests passed");
return 0;
} catch (const std::exception& error) {