From 1baf077a7e32b8d5a397b1bfed22813d8265531d Mon Sep 17 00:00:00 2001 From: raghav314 Date: Fri, 9 Oct 2026 15:02:06 +0530 Subject: [PATCH 1/2] fix(agc): clear a multisampled target on a uniform fill of its color memory A multisampled target's samples live only in its host image, so a fill of its color memory never reached them: a title that clears its MSAA targets that way (with no CMASK) kept drawing over the previous frame. A uniform fill over the whole color memory now clears the host image to that value at the target's next use, the same way a CMASK fast clear does, and a pending CMASK clear still takes precedence. The color memory is sized with addrlib's block dimensions for the sample count (ComputeThinBlockDimension): a 64 KiB block holds width x height x samples elements, laid out width first for 1x and 4x. A fill over part of it, over a target whose footprint is not computed, or with a value that has no exact clear value in the format throws at the next use. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/MultisampleTarget.hpp | 16 +++++ .../Graphics/src/MultisampleTarget.cpp | 66 ++++++++++++++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 56 ++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 5d9d14728..1d5440076 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include +#include #include namespace AgcDriver::Graphics { @@ -30,6 +31,21 @@ private: std::string refusal; }; +class MultisampledColorFill { +public: + explicit MultisampledColorFill(const ColorTarget& target); + void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); + std::optional TakeFill(); + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + std::optional filled; + std::string refusal; +}; + +std::uint64_t MultisampledColorBytes(const ColorTarget& target); +bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 743b469cd..5e09b6fdb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -8,10 +8,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -43,7 +46,7 @@ void recordCommands(const Context& context, Record&& record) { class MultisampleTarget { public: - MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes) { + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { this->context.bufferPool.reset(); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); @@ -105,8 +108,15 @@ public: MultisampleTarget& operator=(const MultisampleTarget&) = delete; void ApplyFastClear(const ColorTarget& use) { - if (!cmask.TakeClear()) return; - const auto clear = fastClearColor(use); + const auto fill = colorFill.TakeFill(); + VkClearColorValue clear{}; + if (cmask.TakeClear()) { + clear = fastClearColor(use); + } else if (fill) { + Require(MultisampledFillClearColor(use, *fill, clear), "a fill over the color memory of a multisampled target has no exact clear value in its format"); + } else { + return; + } recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -156,6 +166,7 @@ public: VkImageView view = VK_NULL_HANDLE; VkRenderPass resolvePass = VK_NULL_HANDLE; MultisampledCmask cmask; + MultisampledColorFill colorFill; private: void release() noexcept { @@ -272,6 +283,54 @@ bool MultisampledCmask::TakeClear() { return true; } +std::uint64_t MultisampledColorBytes(const ColorTarget& target) { + const auto blockLog2 = target.tileMode == ColorTileMode::Standard4KB ? 12u : target.tileMode == ColorTileMode::Standard64KB || target.tileMode == ColorTileMode::RenderTarget ? 16u : 0u; + if (blockLog2 == 0 || !std::has_single_bit(target.elementBytes) || !std::has_single_bit(target.samples)) return 0; + const auto samplesLog2 = static_cast(std::countr_zero(target.samples)); + const auto elementsLog2 = blockLog2 - static_cast(std::countr_zero(target.elementBytes)) - samplesLog2; + const auto widthLog2 = (elementsLog2 + ((samplesLog2 & 1u) == 0 ? 1u : 0u)) / 2u; + const std::uint64_t blockWidth = std::uint64_t{1} << widthLog2; + const std::uint64_t blockHeight = std::uint64_t{1} << (elementsLog2 - widthLog2); + return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2; +} + +bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) { + const std::array pattern{fill, fill, fill, fill}; + VkFormat unorm = VK_FORMAT_UNDEFINED; + switch (target.format) { + case VK_FORMAT_R8G8B8A8_SRGB: unorm = VK_FORMAT_R8G8B8A8_UNORM; break; + case VK_FORMAT_B8G8R8A8_SRGB: unorm = VK_FORMAT_B8G8R8A8_UNORM; break; + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: unorm = VK_FORMAT_A8B8G8R8_UNORM_PACK32; break; + default: return ClearColorForTexel(target.format, target.elementBytes, pattern, clear); + } + if (!ClearColorForTexel(unorm, target.elementBytes, pattern, clear)) return false; + return std::all_of(clear.float32, clear.float32 + 3, [](float value) { return value == 0.0f || value == 1.0f; }); +} + +MultisampledColorFill::MultisampledColorFill(const ColorTarget& target) : address(target.address), bytes(MultisampledColorBytes(target)) {} + +void MultisampledColorFill::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + if (bytes == 0 ? fillAddress > address || address >= fillAddress + fillBytes : fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + char text[192]; + if (bytes == 0) { + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx covers the color memory at 0x%llx of a multisampled target whose footprint is not modeled", static_cast(fillAddress), fillBytes, static_cast(address)); + refusal = text; + return; + } + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the color memory 0x%llx+0x%llx of a multisampled target, leaving its samples mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + filled = pattern; +} + +std::optional MultisampledColorFill::TakeFill() { + Require(refusal.empty(), refusal); + return std::exchange(filled, std::nullopt); +} + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); return acquire(context, target).view; @@ -302,6 +361,7 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(targetsMutex()); for (const auto& target : targets()) { + target->colorFill.NoteFill(address, bytes, pattern); const auto before = target->cmask.Keys(); target->cmask.NoteFill(address, bytes, pattern); if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 22b5c62dd..a5f296906 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2149,6 +2149,62 @@ void multisampleTests() { MultisampledCmask unaddressed; unaddressed.NoteFill(0, 0x1000, 0); Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); + + using AgcDriver::Graphics::ColorTarget; + using AgcDriver::Graphics::ColorTileMode; + using AgcDriver::Graphics::MultisampledColorBytes; + using AgcDriver::Graphics::MultisampledColorFill; + using AgcDriver::Graphics::MultisampledFillClearColor; + ColorTarget halfFloat{}; + halfFloat.address = 0x40000000; + halfFloat.extent = {960, 540}; + halfFloat.format = VK_FORMAT_R16G16B16A16_SFLOAT; + halfFloat.tileMode = ColorTileMode::RenderTarget; + halfFloat.elementBytes = 8; + halfFloat.samples = 4; + Require(MultisampledColorBytes(halfFloat) == 0xff0000, "a 4x 960x540 8-byte target is not 64x32 pixels per 64 KiB block"); + auto srgb = halfFloat; + srgb.extent = {1536, 1536}; + srgb.format = VK_FORMAT_R8G8B8A8_SRGB; + srgb.elementBytes = 4; + Require(MultisampledColorBytes(srgb) == 0x2400000, "a 4x 1536x1536 4-byte target is not 64x64 pixels per 64 KiB block"); + auto narrow = srgb; + narrow.extent = {130, 60}; + narrow.samples = 2; + Require(MultisampledColorBytes(narrow) == 0x30000, "a 2x 4-byte target is not 64x128 pixels per 64 KiB block"); + narrow.samples = 8; + Require(MultisampledColorBytes(narrow) == 0x50000, "an 8x 4-byte target is not 32x64 pixels per 64 KiB block"); + srgb.extent = {100, 100}; + Require(MultisampledColorBytes(srgb) == 0x40000, "a 4x 100x100 4-byte target does not span 2x2 64 KiB blocks"); + srgb.tileMode = ColorTileMode::Standard4KB; + Require(MultisampledColorBytes(srgb) == 0x31000, "a 4x 4-byte target is not 16x16 pixels per 4 KiB block"); + srgb.tileMode = ColorTileMode::Linear; + Require(MultisampledColorBytes(srgb) == 0, "a linear multisampled target has a footprint"); + + MultisampledColorFill fill(halfFloat); + Require(!fill.TakeFill(), "an unfilled target cleared"); + fill.NoteFill(0x40000000, 0xff0000, 0); + const auto zero = fill.TakeFill(); + Require(zero && *zero == 0 && !fill.TakeFill(), "a fill of the whole color memory did not clear once"); + fill.NoteFill(0x40ff0000, 0x10000, 0); + fill.NoteFill(0x3fff0000, 0x10000, 0); + Require(!fill.TakeFill(), "a fill next to the color memory cleared it"); + fill.NoteFill(0x40000000, 0xfe0000, 0); + expectFailure([&] { fill.TakeFill(); }, "partly covers"); + fill.NoteFill(0x3fff0000, 0x1010000, 0x3c003c00u); + const auto one = fill.TakeFill(); + Require(one && *one == 0x3c003c00u, "a fill over and around the color memory did not clear it"); + MultisampledColorFill linear(srgb); + linear.NoteFill(0x40001000, 0x1000, 0); + Require(!linear.TakeFill(), "a fill past the start of a target with no modeled footprint was taken"); + linear.NoteFill(0x40000000, 0x1000, 0); + expectFailure([&] { linear.TakeFill(); }, "footprint is not modeled"); + + VkClearColorValue clear{}; + Require(MultisampledFillClearColor(halfFloat, 0x3c003c00u, clear) && clear.float32[0] == 1.0f && clear.float32[3] == 1.0f, "a half-float fill of 1.0 did not clear to 1.0"); + Require(MultisampledFillClearColor(srgb, 0, clear) && clear.float32[0] == 0.0f && clear.float32[3] == 0.0f, "an sRGB fill of 0 did not clear to 0"); + Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it"); + Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared"); } void DepthClipTests() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8c8b96a70..37d98e505 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -72,6 +72,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. +- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. From e322c6e06443f6d07153b741800bfd6d058c8ce1 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 13 Oct 2026 15:48:03 +1300 Subject: [PATCH 2/2] fix(agc): a pending CMASK clear takes precedence over a refused color fill ApplyFastClear took the color fill before the CMASK clear, so a partial fill of a multisampled target's color memory threw at the next use even when a CMASK fast clear was pending over the whole target. The CB reads a cleared tile's color from the clear registers whatever the memory holds, so the CMASK clear is applied and the recorded fill dropped. --- .../Graphics/include/MultisampleTarget.hpp | 2 ++ .../Graphics/src/MultisampleTarget.cpp | 26 +++++++++++++------ .../prx/libSceAgcDriver/tests/Graphics.cpp | 16 ++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 1d5440076..50674497b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -36,6 +36,7 @@ public: explicit MultisampledColorFill(const ColorTarget& target); void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); std::optional TakeFill(); + void Discard(); private: std::uint64_t address = 0; @@ -46,6 +47,7 @@ private: std::uint64_t MultisampledColorBytes(const ColorTarget& target); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); +bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 5e09b6fdb..cf950027a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -108,15 +108,8 @@ public: MultisampleTarget& operator=(const MultisampleTarget&) = delete; void ApplyFastClear(const ColorTarget& use) { - const auto fill = colorFill.TakeFill(); VkClearColorValue clear{}; - if (cmask.TakeClear()) { - clear = fastClearColor(use); - } else if (fill) { - Require(MultisampledFillClearColor(use, *fill, clear), "a fill over the color memory of a multisampled target has no exact clear value in its format"); - } else { - return; - } + if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -331,6 +324,23 @@ std::optional MultisampledColorFill::TakeFill() { return std::exchange(filled, std::nullopt); } +void MultisampledColorFill::Discard() { + refusal.clear(); + filled.reset(); +} + +bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear) { + if (cmask.TakeClear()) { + colorFill.Discard(); + clear = fastClearColor(use); + return true; + } + const auto fill = colorFill.TakeFill(); + if (!fill) return false; + Require(MultisampledFillClearColor(use, *fill, clear), "a fill over the color memory of a multisampled target has no exact clear value in its format"); + return true; +} + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); return acquire(context, target).view; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a5f296906..09275b49c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2205,6 +2205,22 @@ void multisampleTests() { Require(MultisampledFillClearColor(srgb, 0, clear) && clear.float32[0] == 0.0f && clear.float32[3] == 0.0f, "an sRGB fill of 0 did not clear to 0"); Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it"); Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared"); + + using AgcDriver::Graphics::TakeMultisampledClear; + halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u}; + MultisampledCmask clearedCmask(0x50000000, 0x1000); + MultisampledColorFill pending(halfFloat); + Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a target with no CMASK clear and no fill cleared"); + pending.NoteFill(0x40000000, 0xff0000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 0.0f, "a fill of the whole color memory did not clear to its value"); + pending.NoteFill(0x40000000, 0xfe0000, 0); + expectFailure([&] { TakeMultisampledClear(clearedCmask, pending, halfFloat, clear); }, "partly covers"); + clearedCmask.NoteFill(0x50000000, 0x1000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f, "a pending CMASK clear did not take precedence over a partial fill of the color memory"); + Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a partial fill of the color memory outlived the CMASK clear over it"); + pending.NoteFill(0x40000000, 0xff0000, 0); + clearedCmask.NoteFill(0x50000000, 0x1000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color"); } void DepthClipTests() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 37d98e505..83f0b7679 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -72,7 +72,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. -- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. +- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware.