From 12b20e17f615f099b8ffa2aeea4872844cdff71f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 03:19:46 +0000 Subject: [PATCH] fix(agc): read a multisampled target's CMASK by its MSAA codes A multisampled color target with a CMASK fast clear seeds its state from the CMASK when it is first used, and later fills of the CMASK update it. Both read the bytes as DCC keys: 0x00 is a clear, 0xff uncompressed, and anything else refuses ("reads as mixed"). But an MSAA CMASK nibble is FMASK code | color code << 2 (Mesa ac_descriptors.h CMASK_MSAA_CODE), so 0xcc (FMASK cleared to fragment 0, color expanded), and the 2x and 4x "FMASK uncompressed, color expanded" codes 0xdd and 0xee, are states with no pending fast clear that were refused. Ruffle (PPSA68091) clears its target's CMASK to 0xcc every frame and stopped on the refusal whenever that clear landed before the target's first use. The seed now reads the uniform CMASK byte (after pending writes, like the key read) and both paths classify it by those codes: 0x00 (register color clear, FMASK cleared) is the fast clear as before, any color-expanded code (0xc to 0xf in both nibbles) is "no fast clear", and other codes, or bytes that differ, are still refused. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/DccMetadata.hpp | 1 + .../Graphics/include/MultisampleTarget.hpp | 4 ++- .../Graphics/src/DccMetadata.cpp | 19 ++++++++++++ .../Graphics/src/MultisampleTarget.cpp | 30 ++++++++++++------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 22 ++++++++++++-- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index d2e9dff3a6..5e05ec233f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -39,6 +39,7 @@ bool IsDccClear(DccKeys keys); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount); std::optional KnownPendingDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); +std::optional CurrentUniformMetadataByte(std::uint64_t address, std::size_t bytes); // Stores "uncompressed" keys over the surface's metadata on the CPU (a guest memory write: it waits // for recorded GPU work that writes the keys first). void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 98d65e8c03..3aece028bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -17,12 +17,14 @@ class StorageTexture; class Texture; struct GuestTextureResource; +std::optional MultisampledCmaskKeys(std::uint8_t code); + class MultisampledCmask { public: MultisampledCmask() = default; MultisampledCmask(std::uint64_t cmaskAddress, std::uint64_t cmaskBytes) : address(cmaskAddress), bytes(cmaskBytes) {} void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); - void SeedKeys(DccKeys seeded); + void SeedCode(std::optional code); void CheckAddress(std::uint64_t cmaskAddress) const; bool TakeClear(); DccKeys Keys() const { return keys; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index f8f4d371ec..7491f9bc8a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -580,6 +580,25 @@ DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, st return currentDccKeys(metaAddress, keyCount, memoized); } +std::optional CurrentUniformMetadataByte(std::uint64_t address, std::size_t bytes) { + if (address == 0 || bytes == 0 || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return std::nullopt; + if (GuestMemory::GpuMutex().HeldByThisThread()) { + if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); + if (auto* recorder = Recorder::Active(); recorder != nullptr && (recorder->PendingWriteOverlaps(address, bytes) || AnyShadowedOverlaps(address, bytes))) { + const auto landed = KeysAfterPendingWrites(*recorder, address, bytes); + if (landed.mixed) return std::nullopt; + if (!landed.unknown && landed.value >= 0 && !AnyShadowedOverlaps(address, bytes)) return static_cast(landed.value); + Recorder::CountSync(2); + recorder->Sync(); + } + } + std::vector data(bytes); + GuestMemory::Read(address, data); + const auto first = data.front(); + if (!std::all_of(data.begin(), data.end(), [&](std::byte value) { return value == first; })) return std::nullopt; + return std::to_integer(first); +} + std::optional KnownPendingDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { const auto count = DccKeyBytes(surfaceBytes); if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 5fc7544a28..a1abe86657 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -454,7 +454,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); guard(context, use, created.active, created.metadataValid); - if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); + if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedCode(CurrentUniformMetadataByte(use.cmaskAddress, static_cast(use.cmaskBytes))); created.ApplyFastClear(use); return created; } @@ -523,6 +523,14 @@ void UpdateResidentImageUnmapObserver_nid_no_patch() { GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(anyGuarded().load(std::memory_order_acquire) || HasResidentDepthGuards_nid_no_patch() ? &retireUnmapped : nullptr); } +std::optional MultisampledCmaskKeys(std::uint8_t code) { + const auto tile = code & 0xfu; + if ((code >> 4u) != tile) return std::nullopt; + if (tile == 0u) return DccKeys::Clear0000; + if ((tile >> 2u) == 3u) return DccKeys::Uncompressed; + return std::nullopt; +} + void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; if (fillAddress > address || fillAddress + fillBytes < address + bytes) { @@ -532,24 +540,24 @@ void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillByte return; } refusal.clear(); - if (pattern == 0) { - keys = DccKeys::Clear0000; - } else if (pattern == 0xffffffffu) { - keys = DccKeys::Uncompressed; + const auto code = static_cast(pattern); + if (const auto filled = MultisampledCmaskKeys(code); filled && pattern == code * 0x01010101u) { + keys = *filled; } else { - char text[192]; - std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither fast-cleared 0 nor expanded 0xffffffff)", static_cast(address), static_cast(bytes), pattern); + char text[224]; + std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither a register color clear 0 nor a color-expanded code such as 0xcc or 0xff)", static_cast(address), static_cast(bytes), pattern); refusal = text; } } -void MultisampledCmask::SeedKeys(DccKeys seeded) { - if (seeded == DccKeys::Clear0000 || seeded == DccKeys::Uncompressed) { - keys = seeded; +void MultisampledCmask::SeedCode(std::optional code) { + if (const auto seeded = code ? MultisampledCmaskKeys(*code) : std::nullopt) { + keys = *seeded; return; } char text[192]; - std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as %s, which is not modeled", static_cast(address), DccKeysName(seeded)); + if (code) std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as code 0x%02x, which is not modeled", static_cast(address), *code); + else std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as mixed, which is not modeled", static_cast(address)); refusal = text; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cbba137501..773bc2986b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3061,12 +3061,28 @@ void multisampleTests() { cmask.CheckAddress(0x10000); expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000"); MultisampledCmask seeded(0x30000, 0x1000); - seeded.SeedKeys(DccKeys::Clear0000); + seeded.SeedCode(0x00u); Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); Require(!seeded.TakeClear(), "a subsequent use applied the clear again"); MultisampledCmask mixedSeed(0x30000, 0x1000); - mixedSeed.SeedKeys(DccKeys::Mixed); - expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); + mixedSeed.SeedCode(std::nullopt); + expectFailure([&] { mixedSeed.TakeClear(); }, "reads as mixed, which is not modeled"); + for (const std::uint8_t expanded : {0xccu, 0xddu, 0xeeu, 0xffu}) { + MultisampledCmask expandedSeed(0x30000, 0x1000); + expandedSeed.SeedCode(expanded); + Require(!expandedSeed.TakeClear() && expandedSeed.Keys() == DccKeys::Uncompressed, "a color-expanded CMASK seed applied a fast clear or was refused"); + MultisampledCmask expandedFill(0x30000, 0x1000); + expandedFill.NoteFill(0x30000, 0x1000, expanded * 0x01010101u); + Require(!expandedFill.TakeClear() && expandedFill.Keys() == DccKeys::Uncompressed, "a color-expanded CMASK fill applied a fast clear or was refused"); + } + for (const std::uint8_t unmodeled : {0x11u, 0x33u, 0x44u, 0x88u, 0xcdu}) { + MultisampledCmask unmodeledSeed(0x30000, 0x1000); + unmodeledSeed.SeedCode(unmodeled); + expectFailure([&] { unmodeledSeed.TakeClear(); }, "reads as code 0x"); + } + MultisampledCmask unevenFill(0x30000, 0x1000); + unevenFill.NoteFill(0x30000, 0x1000, 0xccccccdcu); + expectFailure([&] { unevenFill.TakeClear(); }, "leaves its tiles mixed"); MultisampledCmask unaddressed; unaddressed.NoteFill(0, 0x1000, 0); Require(!unaddressed.TakeClear(), "a target without a CMASK cleared");