From e4ced630567284008c285a20d0fd7dffb9009732 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 7 Oct 2026 09:54:10 +0200 Subject: [PATCH 1/7] feat(agc): render into CMASK fast-clear color targets Draws into a color target with CB_COLOR_INFO FAST_CLEAR (bit 13) were rejected. The CMASK keeps one nibble per 8x8 tile: 0 means the tile is fast-cleared and reads as CB_COLOR_CLEAR_WORD, 0xF means expanded (the texels hold the color). The texture units and display scanout do not read it, so a title runs a fast-clear eliminate pass before sampling or flipping such a target. DecodeColorBuffer now takes the CMASK address (CB_COLOR0_CMASK and CB_COLOR0_CMASK_BASE_EXT) and the bytes one nibble per 8x8 tile spans. Before a draw renders into the target, and in a fast-clear eliminate pass, a CMASK whose nibbles are all 0 is materialized: the target is filled with the clear texel (FillClear on its resident image, else guest memory on the CPU) and the CMASK bytes are stored as expanded, through the same GPU-ordered store as the DCC "uncompressed" keys (0xFF either way). The result equals the draw followed by the eliminate pass. An all-expanded CMASK changes nothing; a mixed one throws, as do fast clears of DCC, mipmapped, 3D and array targets. PPSA14632 (LEGO Horizon Adventures) fast-clears its two 3840x2160 display buffers through CMASK, so its final composite draw was dropped on every frame and only black was presented. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/State.hpp | 2 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 61 ++++++++++++++----- .../libSceAgcDriver/Graphics/src/State.cpp | 10 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 52 ++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 111 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 8367436d979..7d3ab0af833 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -41,6 +41,8 @@ struct ColorTarget { // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; + std::uint64_t cmaskAddress = 0; + std::size_t cmaskBytes = 0; std::uint64_t surfaceAddress = 0; VkExtent2D surfaceExtent{}; std::uint32_t mipCount = 1; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index aafdb1e31e6..dd69b5b0907 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -90,15 +90,18 @@ bool clearToTexel(StorageTexture& image, const std::array& texel, return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal); } +void writeTexels(const ColorTarget& color, const std::array& texel) { + const auto elementBytes = static_cast(color.elementBytes); + std::vector texels(color.bytes); + for (std::size_t offset = 0; offset + elementBytes <= texels.size(); offset += elementBytes) std::memcpy(texels.data() + offset, texel.data(), elementBytes); + GuestMemory::Write(color.address, texels); +} + void storeClearTexels(const Context& context, const ColorTarget& color, const std::array& texel) { StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization"); const auto keys = ReadDccKeys(color.dccAddress, color.bytes); if (!IsDccClear(keys)) return; - const auto current = keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys); - const auto elementBytes = static_cast(color.elementBytes); - std::vector texels(color.bytes); - for (std::size_t offset = 0; offset + elementBytes <= texels.size(); offset += elementBytes) std::memcpy(texels.data() + offset, current.data(), elementBytes); - GuestMemory::Write(color.address, texels); + writeTexels(color, keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys)); MarkDccUncompressed(context, color.dccAddress, color.bytes); } @@ -125,6 +128,23 @@ void materializeRegisterClear(const Context& context, const ColorTarget& color, resident.Refresh(); } +bool materializeCmaskClear(const Context& context, const ColorTarget& color, StorageTexture* resident) { + if (color.cmaskAddress == 0) return false; + const auto metadataBytes = static_cast(color.cmaskBytes) * 256u; + const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); + if (state == DccKeys::Uncompressed) return false; + Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")"); + const auto texel = clearTexel(color, DccKeys::ClearRegister); + const char* refusal = nullptr; + if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) { + StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization"); + writeTexels(color, texel); + if (resident != nullptr) resident->Refresh(); + } + MarkDccUncompressed(context, color.cmaskAddress, metadataBytes); + return true; +} + void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.srcAccessMask = sourceAccess; @@ -895,6 +915,7 @@ std::shared_ptr refreshResidentTarget(const Context& context, co std::lock_guard lock(reportMutex); if (reported.insert(color.address).second) std::fprintf(stderr, "[gpu] color target 0x%llx stays non-resident: %s\n", static_cast(color.address), error.what()); } + if (resident != nullptr) materializeCmaskClear(context, color, resident.get()); if (profile) { const auto lookupUs = std::chrono::duration(std::chrono::steady_clock::now() - lookupStart).count(); ++outcome.targetLookups; @@ -1527,6 +1548,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& targetViews.push_back(binding.resident->AttachmentView(color.format, color.mip, color.depthSlice)); continue; } + materializeCmaskClear(context, color, nullptr); Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); if (binding.gpuTiling) { @@ -2067,22 +2089,33 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con return result; } +namespace { + +std::shared_ptr metadataPassResident(const Context& context, const ColorTarget& color) { + if ((color.tileMode != ColorTileMode::RenderTarget && color.tileMode != ColorTileMode::Standard4KB) || context.detiler == nullptr) return nullptr; + std::shared_ptr resident; + try { + resident = CachedStorageSurface(context, SurfaceForTarget(color)); + } catch (const std::exception&) { + return nullptr; + } + return resident != nullptr && resident->GuestBytes() == color.bytes ? resident : nullptr; +} + +} + void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) { for (const auto& color : pass.targets) { + if (color.cmaskAddress != 0) { + const auto resident = metadataPassResident(context, color); + if (materializeCmaskClear(context, color, resident.get()) && resident != nullptr) resident->MarkDirty(); + } if (color.dccAddress == 0) continue; auto keys = CurrentDccKeys(color.dccAddress, color.bytes); if (keys == DccKeys::Uncompressed) continue; Require(IsDccClear(keys), std::string("CB metadata pass over DCC keys that are ") + DccKeysName(keys) + " (per-block metadata is not modeled)"); const auto texel = clearTexel(color, keys); - std::shared_ptr resident; - if ((color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB) && context.detiler != nullptr) { - try { - resident = CachedStorageSurface(context, SurfaceForTarget(color)); - } catch (const std::exception&) { - resident = nullptr; - } - if (resident != nullptr && resident->GuestBytes() != color.bytes) resident = nullptr; - } + const auto resident = metadataPassResident(context, color); if (resident != nullptr) { const char* refusal = nullptr; const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cab2912f02b..ae2b71d86ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -628,7 +628,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 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); }() + ")"); + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 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 == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); @@ -676,6 +676,14 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } + if ((info & 0x2000u) != 0) { + Require((info & 0x10000000u) == 0, "CMASK fast clears of a DCC color target are unsupported"); + Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + const auto cmaskHigh = find(cx, 0x398 + slot); + color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); + Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported"); + color.cmaskBytes = (static_cast((color.extent.width + 7u) / 8u) * ((color.extent.height + 7u) / 8u) + 1u) / 2u; + } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { const auto dccHigh = find(cx, 0x3a8 + slot); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9be7d4dcec2..78dcdb8fc0a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -734,6 +734,57 @@ void metadataPassTests() { } } +alignas(256) std::array cmaskMemory{}; + +void cmaskTests() { + using AgcDriver::Graphics::DecodeColorBuffer; + auto queue = makeState(); + queue.context[0x31c] |= 0x2000; + const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); + queue.context[0x31f] = static_cast(cmaskAddress >> 8u); + queue.context[0x398] = static_cast(cmaskAddress >> 40u); + queue.context[0x323] = 0x11223344; + queue.context[0x324] = 0; + const auto target = DecodeColorBuffer(queue.context, 0); + Require(target.cmaskAddress == cmaskAddress && target.cmaskBytes == cmaskMemory.size(), "the fast-clear target lost its CMASK"); + Require(DecodeColorBuffer(makeState().context, 0).cmaskAddress == 0, "a target without FAST_CLEAR got a CMASK"); + auto compressed = queue; + compressed.context[0x31c] |= 0x10000000; + expectFailure([&] { DecodeColorBuffer(compressed.context, 0); }, "DCC color target"); + auto mipmapped = queue; + mipmapped.context[0x3b0] |= 1u << 28u; + expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); + auto unaddressed = queue; + unaddressed.context[0x31f] = 0; + unaddressed.context[0x398] = 0; + expectFailure([&] { DecodeColorBuffer(unaddressed.context, 0); }, "without a CMASK address"); + + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0020; + auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].cmaskAddress == cmaskAddress, "the metadata pass lost the target's CMASK"); + const AgcDriver::Graphics::Context context{}; + const auto texels = [&](std::uint32_t value) { + for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { + std::uint32_t texel = 0; + std::memcpy(&texel, colorMemory.data() + offset, 4); + if (texel != value) return false; + } + return true; + }; + const auto cmaskIs = [&](std::uint8_t value) { return std::all_of(cmaskMemory.begin(), cmaskMemory.end(), [&](std::uint8_t byte) { return byte == value; }); }; + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + cmaskMemory.fill(0); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(texels(0x11223344) && cmaskIs(0xff), "a CMASK fast clear was not eliminated into the texels"); + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(texels(0x5a5a5a5au) && cmaskIs(0xff), "a pass over an expanded CMASK changed the texels"); + cmaskMemory = {0x00, 0xff, 0x00, 0x00}; + expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not all fast-cleared or all expanded"); + Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -2037,6 +2088,7 @@ int main() { DisabledColorTests(); CompactedExportTests(); metadataPassTests(); + cmaskTests(); ShaderStageTests(); PixelInputLayoutTests(); InitialContextTests(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c4ee68aa942..b514d94c224 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,6 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. +- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over one nibble per 8x8 tile of the target from its start; the console's CMASK layout (pipe alignment, padding) is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. A mixed CMASK throws, as do fast clears of DCC, mipmapped, 3D and array color targets - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). From 44d855a6966c28d23c0eee65ba68429644e3d06e Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 7 Oct 2026 13:02:23 +0200 Subject: [PATCH 2/7] feat(agc): accept CMASK fast-clear DCC targets whose CMASK is expanded A color target with both DCC and CB_COLOR_INFO.FAST_CLEAR keeps its fast clear in the DCC keys and fills its CMASK with 0xF (expanded), as radv does (radv_get_cmask_fast_clear_value) and as PPSA21567 does (CB_COLOR_INFO 0x10042730, reported on #419). Such targets now decode; their clear is materialized by the existing DCC path. A DCC target's CMASK that is not all expanded throws. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 1 - .../prx/libSceAgcDriver/tests/Graphics.cpp | 19 ++++++++++++++++--- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index dd69b5b0907..3931ec22b15 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -133,6 +133,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, Sto const auto metadataBytes = static_cast(color.cmaskBytes) * 256u; const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); if (state == DccKeys::Uncompressed) return false; + Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")"); Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")"); const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ae2b71d86ed..7906c803626 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -677,7 +677,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } if ((info & 0x2000u) != 0) { - Require((info & 0x10000000u) == 0, "CMASK fast clears of a DCC color target are unsupported"); Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); const auto cmaskHigh = find(cx, 0x398 + slot); color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78dcdb8fc0a..954042ac760 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -748,9 +748,6 @@ void cmaskTests() { const auto target = DecodeColorBuffer(queue.context, 0); Require(target.cmaskAddress == cmaskAddress && target.cmaskBytes == cmaskMemory.size(), "the fast-clear target lost its CMASK"); Require(DecodeColorBuffer(makeState().context, 0).cmaskAddress == 0, "a target without FAST_CLEAR got a CMASK"); - auto compressed = queue; - compressed.context[0x31c] |= 0x10000000; - expectFailure([&] { DecodeColorBuffer(compressed.context, 0); }, "DCC color target"); auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); @@ -783,6 +780,22 @@ void cmaskTests() { cmaskMemory = {0x00, 0xff, 0x00, 0x00}; expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not all fast-cleared or all expanded"); Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); + + queue.context[0x31c] |= 0x10000000; + const auto keysAddress = reinterpret_cast(dccKeys.data()); + queue.context[0x325] = static_cast(keysAddress >> 8u); + queue.context[0x3a8] = static_cast(keysAddress >> 40u); + pass = AgcDriver::Graphics::DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].cmaskAddress == cmaskAddress && pass->targets[0].dccAddress == keysAddress, "a DCC fast-clear target lost its CMASK or DCC keys"); + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + cmaskMemory.fill(0xff); + dccKeys.fill(0x20); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(texels(0x11223344) && cmaskIs(0xff) && std::ranges::all_of(dccKeys, [](std::uint8_t key) { return key == 0xff; }), "a DCC fast clear under an expanded CMASK was not eliminated through its keys"); + std::memset(colorMemory.data(), 0x5a, colorMemory.size()); + cmaskMemory.fill(0); + expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "DCC color target that is not all expanded"); + Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target"); } void DepthClipTests() { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b514d94c224..7a7f4cc6c15 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,7 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over one nibble per 8x8 tile of the target from its start; the console's CMASK layout (pipe alignment, padding) is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. A mixed CMASK throws, as do fast clears of DCC, mipmapped, 3D and array color targets +- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over one nibble per 8x8 tile of the target from its start; the console's CMASK layout (pipe alignment, padding) is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). From 1c848903a0d06dfd63289cbd566a29361788d849 Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 7 Oct 2026 13:58:48 -0500 Subject: [PATCH 3/7] fix(agc): size CMASK by whole 1024x512 metablocks A CMASK holds 4 KiB per 1024x512-pixel metablock, not one nibble per 8x8 tile of the target: 1920x1080 spans 0x6000 bytes, as titles clear it, not 16200. The swizzled nibbles of the lower-right tiles lie past the packed size, so the fast-clear check missed them and the expanded store left them cleared. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp | 1 + core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 9 ++++++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 7 +++++-- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 7d3ab0af833..aac51b83794 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -102,6 +102,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); std::array ExportMappings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); +std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); struct ColorMetadataPass { enum class Mode { EliminateFastClear, DccDecompress }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 7906c803626..f42cd074687 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -617,6 +617,13 @@ std::array ExportMappings(const State& state) { return mappings; } +std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height) { + constexpr std::size_t metablockWidth = 1024; + constexpr std::size_t metablockHeight = 512; + constexpr std::size_t metablockBytes = 4096; + return ((width + metablockWidth - 1) / metablockWidth) * ((height + metablockHeight - 1) / metablockHeight) * metablockBytes; +} + ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto stride = slot * 0xfu; ColorTarget color{}; @@ -681,7 +688,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto cmaskHigh = find(cx, 0x398 + slot); color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported"); - color.cmaskBytes = (static_cast((color.extent.width + 7u) / 8u) * ((color.extent.height + 7u) / 8u) + 1u) / 2u; + color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 954042ac760..8f2e42bf846 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -734,7 +734,7 @@ void metadataPassTests() { } } -alignas(256) std::array cmaskMemory{}; +alignas(256) std::array cmaskMemory{}; void cmaskTests() { using AgcDriver::Graphics::DecodeColorBuffer; @@ -748,6 +748,8 @@ void cmaskTests() { const auto target = DecodeColorBuffer(queue.context, 0); Require(target.cmaskAddress == cmaskAddress && target.cmaskBytes == cmaskMemory.size(), "the fast-clear target lost its CMASK"); Require(DecodeColorBuffer(makeState().context, 0).cmaskAddress == 0, "a target without FAST_CLEAR got a CMASK"); + using AgcDriver::Graphics::CmaskBytes; + Require(CmaskBytes(1920, 1080) == 0x6000 && CmaskBytes(960, 544) == 0x2000 && CmaskBytes(800, 450) == 0x1000 && CmaskBytes(1024, 512) == 0x1000 && CmaskBytes(3840, 2160) == 0x14000, "a CMASK does not span whole 1024x512 metablocks"); auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); @@ -777,7 +779,8 @@ void cmaskTests() { std::memset(colorMemory.data(), 0x5a, colorMemory.size()); AgcDriver::Graphics::RunColorMetadataPass(context, *pass); Require(texels(0x5a5a5a5au) && cmaskIs(0xff), "a pass over an expanded CMASK changed the texels"); - cmaskMemory = {0x00, 0xff, 0x00, 0x00}; + cmaskMemory.fill(0); + cmaskMemory[cmaskMemory.size() - 1] = 0xff; expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not all fast-cleared or all expanded"); Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7a7f4cc6c15..c57ce2cb45d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,7 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over one nibble per 8x8 tile of the target from its start; the console's CMASK layout (pipe alignment, padding) is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target +- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). From 1cd113e52a6d120d0e442f8f7fc38e77212c8723 Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 7 Oct 2026 16:57:03 -0500 Subject: [PATCH 4/7] fix(agc): leave a mixed CMASK as it is instead of refusing the draw With the CMASK sized by whole metablocks, the check covers the swizzled nibbles past the packed size, and titles that partly draw over a fast-cleared target now show a mixed CMASK where the packed check saw a uniform one. Refusing those draws drops them; the target is drawn as it is instead, and only an all-cleared CMASK is materialized. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 4 ++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 3931ec22b15..0f02c6f3238 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -134,7 +134,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, Sto const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); if (state == DccKeys::Uncompressed) return false; Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")"); - Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")"); + if (state != DccKeys::Clear0000) return false; const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 8f2e42bf846..25194172c5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -781,8 +781,8 @@ void cmaskTests() { Require(texels(0x5a5a5a5au) && cmaskIs(0xff), "a pass over an expanded CMASK changed the texels"); cmaskMemory.fill(0); cmaskMemory[cmaskMemory.size() - 1] = 0xff; - expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not all fast-cleared or all expanded"); - Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(texels(0x5a5a5a5au) && cmaskMemory[0] == 0x00 && cmaskMemory[cmaskMemory.size() - 1] == 0xff, "a pass over a mixed CMASK changed the texels or the CMASK"); queue.context[0x31c] |= 0x10000000; const auto keysAddress = reinterpret_cast(dccKeys.data()); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c57ce2cb45d..c24cd1b5f12 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,7 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target +- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK is left as it is: its fast-cleared tiles keep their stale texels instead of the clear value. A DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target throw - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). From 541d1748c90df76013ff9e13d0769b1cd8ba6b8b Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 7 Oct 2026 17:04:07 -0500 Subject: [PATCH 5/7] fix(agc): report once when a mixed CMASK is left as it is Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 0f02c6f3238..e87f6bf1c76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -134,7 +134,11 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, Sto const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); if (state == DccKeys::Uncompressed) return false; Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")"); - if (state != DccKeys::Clear0000) return false; + if (state != DccKeys::Clear0000) { + static std::atomic reported{false}; + if (!reported.exchange(true)) std::fprintf(stderr, "[gpu] CMASK of color target 0x%llx is %s, not all fast-cleared or all expanded: left as it is (fast-cleared tiles keep their stale texels)\n", static_cast(color.address), DccKeysName(state)); + return false; + } const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) { From 2d749227a9460d9eb5588336a4376476ad42ec4a Mon Sep 17 00:00:00 2001 From: Chad Date: Thu, 8 Oct 2026 13:27:01 -0500 Subject: [PATCH 6/7] Revert "fix(agc): report once when a mixed CMASK is left as it is" This reverts commit 541d1748c90df76013ff9e13d0769b1cd8ba6b8b. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e87f6bf1c76..0f02c6f3238 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -134,11 +134,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, Sto const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); if (state == DccKeys::Uncompressed) return false; Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")"); - if (state != DccKeys::Clear0000) { - static std::atomic reported{false}; - if (!reported.exchange(true)) std::fprintf(stderr, "[gpu] CMASK of color target 0x%llx is %s, not all fast-cleared or all expanded: left as it is (fast-cleared tiles keep their stale texels)\n", static_cast(color.address), DccKeysName(state)); - return false; - } + if (state != DccKeys::Clear0000) return false; const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) { From 198b235994b26b2264cdad152ff9e0ab015c5f00 Mon Sep 17 00:00:00 2001 From: Chad Date: Thu, 8 Oct 2026 13:27:01 -0500 Subject: [PATCH 7/7] Revert "fix(agc): leave a mixed CMASK as it is instead of refusing the draw" This reverts commit 1cd113e52a6d120d0e442f8f7fc38e77212c8723. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 4 ++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 0f02c6f3238..3931ec22b15 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -134,7 +134,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, Sto const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes); if (state == DccKeys::Uncompressed) return false; Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")"); - if (state != DccKeys::Clear0000) return false; + Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")"); const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 25194172c5e..8f2e42bf846 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -781,8 +781,8 @@ void cmaskTests() { Require(texels(0x5a5a5a5au) && cmaskIs(0xff), "a pass over an expanded CMASK changed the texels"); cmaskMemory.fill(0); cmaskMemory[cmaskMemory.size() - 1] = 0xff; - AgcDriver::Graphics::RunColorMetadataPass(context, *pass); - Require(texels(0x5a5a5a5au) && cmaskMemory[0] == 0x00 && cmaskMemory[cmaskMemory.size() - 1] == 0xff, "a pass over a mixed CMASK changed the texels or the CMASK"); + expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not all fast-cleared or all expanded"); + Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); queue.context[0x31c] |= 0x10000000; const auto keysAddress = reinterpret_cast(dccKeys.data()); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c24cd1b5f12..c57ce2cb45d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,7 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK is left as it is: its fast-cleared tiles keep their stale texels instead of the clear value. A DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target throw +- [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564).