diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 5d9d14728..50674497b 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,23 @@ 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(); + void Discard(); + +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); +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 b26c25510..123627bbf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -44,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"); @@ -106,8 +108,8 @@ public: MultisampleTarget& operator=(const MultisampleTarget&) = delete; void ApplyFastClear(const ColorTarget& use) { - if (!cmask.TakeClear()) return; - const auto clear = fastClearColor(use); + VkClearColorValue clear{}; + 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}; @@ -157,6 +159,7 @@ public: VkImageView view = VK_NULL_HANDLE; VkRenderPass resolvePass = VK_NULL_HANDLE; MultisampledCmask cmask; + MultisampledColorFill colorFill; private: void release() noexcept { @@ -282,6 +285,71 @@ 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); +} + +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; @@ -312,6 +380,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 5facf38db..b39b1d9cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2158,6 +2158,78 @@ 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"); + + 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 7a38c3d72..783af47a9 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 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) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off.