From a0c51d5778647e3f9b3eb8b028b2d9e257a18ca3 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 22:21:32 +0300 Subject: [PATCH] feat(agc): import uniform multisampled DCC fast clears --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 +- .../Graphics/include/MultisampleTarget.hpp | 3 + .../Graphics/include/State.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 112 +++++++++- .../libSceAgcDriver/Graphics/src/State.cpp | 6 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 60 +++++- .../tests/execution/AlphaToCoverage.cpp | 200 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 + 8 files changed, 384 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f112af6adea..ac0751020cd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1417,18 +1417,28 @@ target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $< if(BUILD_TESTING) add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered8) + set(multisampledDccClearTests) + foreach(samples IN ITEMS 2 4 8) + set(testName "agc_driver_msaa_dcc_clear_${samples}") + add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples}) + list(APPEND multisampledDccClearTests ${testName}) + endforeach() + set_tests_properties(agc_driver_msaa_dcc_state ${multisampledDccClearTests} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(${multisampledDccClearTests} PROPERTIES SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index c7b8e44aba1..98d65e8c03f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -49,6 +50,8 @@ class MultisampledColorFill { }; std::uint64_t MultisampledColorBytes(const ColorTarget& target); +std::size_t MultisampledDccBytes(const ColorTarget& target); +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask); 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); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index b9844764d0b..c2a2c0510ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -67,6 +67,7 @@ struct ColorTarget { bool uintExport = false; std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; + std::uint32_t dccControl = 0; }; struct DepthTarget { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 65e55692aa5..5fc7544a280 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -53,6 +53,8 @@ class MultisampleTarget { public: MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { this->context.bufferPool.reset(); + if (target.dccAddress != 0) metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(target); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); VkFormatProperties properties{}; @@ -78,9 +80,8 @@ class MultisampleTarget { view = created.view; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { recordToGeneral(commands, image); - const VkClearColorValue zero{}; const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &range); + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); }); @@ -176,6 +177,7 @@ class MultisampleTarget { } void ApplyFastClear(const ColorTarget& use) { + checkDccCurrent(use); VkClearColorValue clear{}; if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { @@ -278,6 +280,7 @@ class MultisampleTarget { Context context; std::shared_ptr> active = std::make_shared>(true); + std::shared_ptr> metadataValid; ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; @@ -296,6 +299,55 @@ class MultisampleTarget { VkImageView storageView = VK_NULL_HANDLE; bool storageWritten = false; std::map, std::shared_ptr> textures; + std::vector dccRanges; + + VkClearColorValue initialDccClear(const ColorTarget& use) { + if (use.dccAddress == 0) return {}; + const auto dccBytes = MultisampledDccBytes(use); + Require((use.dccAddress & 4095u) == 0, "multisampled DCC metadata base alignment is not implemented"); + Require(use.cmaskAddress == 0 ? use.cmaskBytes == 0 : use.cmaskBytes == CmaskBytes(use.extent.width, use.extent.height), "multisampled DCC CMASK footprint is invalid"); + const auto colorBytes = MultisampledColorBytes(use); + Require(colorBytes != 0, "multisampled DCC color footprint is not implemented"); + dccRanges.push_back({use.address, static_cast(colorBytes), 0}); + dccRanges.push_back({use.dccAddress, dccBytes, 0}); + if (use.cmaskAddress != 0) dccRanges.push_back({use.cmaskAddress, use.cmaskBytes, 0}); + for (auto& range : dccRanges) { + GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); + Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); + } + for (std::size_t first = 0; first < dccRanges.size(); ++first) { + for (std::size_t second = first + 1; second < dccRanges.size(); ++second) { + const auto& a = dccRanges[first]; + const auto& b = dccRanges[second]; + Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented"); + } + } + std::vector dcc(dccRanges[1].bytes); + GuestMemory::Read(use.dccAddress, dcc); + std::vector cmask(use.cmaskBytes); + if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); + const auto clear = MultisampledDccClear(use, dcc, cmask); + Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import"); + return clear; + } + + void checkDccCurrent(const ColorTarget& use) const { + if (dccRanges.empty()) { + Require(use.dccAddress == 0, "adding DCC to a resident multisampled color target is not implemented"); + return; + } + Require(metadataValid->load(std::memory_order_acquire), "multisampled DCC metadata was released while its color target remains resident"); + static_cast(MultisampledDccBytes(use)); + Require(use.address == target.address && use.dccAddress == target.dccAddress && use.dccControl == target.dccControl && use.dccAlphaOnMsb == target.dccAlphaOnMsb && use.dccPipeAligned == target.dccPipeAligned && use.cmaskAddress == target.cmaskAddress && use.cmaskBytes == target.cmaskBytes && use.tileMode == target.tileMode && use.elementBytes == target.elementBytes && use.pipeBankXor == target.pipeBankXor, "multisampled DCC target layout or metadata changed"); + for (const auto& range : dccRanges) { + Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented"); + } + } void release() noexcept { textures.clear(); @@ -312,6 +364,7 @@ struct GuardedRange { std::uint64_t bytes; std::uint32_t samples; std::shared_ptr> active; + std::shared_ptr> metadataValid; }; class PrimedSharedMutex : public std::shared_mutex { @@ -345,14 +398,16 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { + if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; - range.active->store(false, std::memory_order_release); + if (!range.metadataValid) range.active->store(false, std::memory_order_release); return true; }); + std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); anyGuarded().store(!guarded().empty(), std::memory_order_release); } -void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active) { +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -365,6 +420,10 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ } std::unique_lock lock(guardedMutex()); guarded().push_back({context.device, target.address, bytes, target.samples, active}); + if (target.dccAddress != 0) { + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); + } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); } @@ -394,8 +453,8 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { } targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); - guard(context, use, created.active); - if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); + guard(context, use, created.active, created.metadataValid); + if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; } @@ -444,7 +503,7 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); - guard(context, use, created.active); + guard(context, use, created.active, created.metadataValid); created.colorFill.NoteFill(use.address, static_cast(bytes), value); created.fromTexture = true; return created; @@ -519,6 +578,45 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target) { return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2; } +std::size_t MultisampledDccBytes(const ColorTarget& target) { + Require(target.samples == 2 || target.samples == 4 || target.samples == 8, "unsupported multisampled DCC fragment count"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.dccPipeAligned, "multisampled DCC requires pipe-aligned SW_64KB_R_X"); + Require(std::has_single_bit(target.elementBytes) && target.elementBytes <= 16, "unsupported multisampled DCC element size"); + Require(target.extent.width != 0 && target.extent.width <= 16384 && target.extent.height != 0 && target.extent.height <= 16384, "invalid multisampled DCC extent"); + Require(target.mipCount == 1 && target.mip == 0 && !target.mipTail && target.depth == 1 && target.depthSlice == 0 && target.arrayLastSlice == 0 && target.arraySlice == 0 && target.layers == 1, "multisampled DCC array, volume or mip contents are not implemented"); + const auto bits = 19u - static_cast(std::countr_zero(target.elementBytes)); + const std::uint64_t width = std::uint64_t{1} << ((bits + 1u) / 2u); + const std::uint64_t height = std::uint64_t{1} << (bits / 2u); + return static_cast(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u); +} + +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask) { + Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented"); + Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch"); + Require(std::ranges::all_of(dcc, [&](std::byte key) { return key == dcc.front(); }), "mixed multisampled DCC metadata is not implemented"); + Require(target.cmaskAddress == 0 ? cmask.empty() && target.cmaskBytes == 0 : cmask.size() == target.cmaskBytes && target.cmaskBytes == CmaskBytes(target.extent.width, target.extent.height), "multisampled DCC CMASK snapshot size mismatch"); + if (!cmask.empty()) { + const auto code = std::to_integer(cmask.front()); + const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu; + Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented"); + } + DccKeys keys; + switch (std::to_integer(dcc.front())) { + case 0x00u: keys = DccKeys::Clear0000; break; + case 0x40u: keys = DccKeys::Clear0001; break; + case 0x80u: keys = DccKeys::Clear1110; break; + case 0xc0u: keys = DccKeys::Clear1111; break; + default: throw std::runtime_error("initial multisampled DCC contents are not a supported uniform fast clear"); + } + std::array texels{}; + Require(FillDccClear(target.format, keys, target.dccAlphaOnMsb, texels), "multisampled DCC clear format is not implemented"); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value"); + return clear; +} + bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) { const std::array pattern{fill, fill, fill, fill}; VkFormat unorm = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index a3c166d3953..e6bc55ce8f0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1440,7 +1440,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; - Require(allowMultisampleDcc || color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); @@ -1540,6 +1539,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; + if (color.samples > 1) { + if (!allowMultisampleDcc) Require(color.tileMode == ColorTileMode::RenderTarget && color.dccAddress != 0 && color.dccPipeAligned && !volume && color.arrayLastSlice == 0 && slice == 0, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); + color.dccControl = read(cx, 0x31e + stride); + if (!allowMultisampleDcc) Require((color.dccControl & 0x3ff80u) == 0, "multisampled DCC color transforms or lossy modes are not implemented"); + } } else { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d1571214727..e2f1c247964 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2938,8 +2938,9 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeState(queue); }, "DCC over a multisampled color target is unsupported"); - expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeState(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); + expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); @@ -2984,7 +2985,8 @@ void multisampleTests() { expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); queue = rasterizedResolve; queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; @@ -3001,13 +3003,15 @@ void multisampleTests() { queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; queue.context[0x31c] = 0x1002e028; queue.context[0x31d] = 0x00012000; + queue.context[0x31e] = 0x00100038u; queue.context[0x325] = 0x2000; queue.context[0x323] = 0x3f800000; queue.context[0x324] = 0x3f800000; @@ -3015,6 +3019,9 @@ void multisampleTests() { auto fmaskPass = DecodeColorMetadataPass(queue); Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); + Require(fmaskPass->targets[0].dccControl == 0x00100038u, "FMASK decompress lost its DCC control"); + queue.context[0x31e] |= 0x80u; + Require(DecodeColorMetadataPass(queue)->targets[0].dccControl == 0x001000b8u, "FMASK decompress hid an unsupported DCC control from the importer"); queue = makeState(); queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; @@ -3126,6 +3133,46 @@ void multisampleTests() { 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 multisampleDccStateTests() { + static std::array color{}; + const auto address = (reinterpret_cast(color.data()) + 65535u) & ~std::uintptr_t{65535u}; + for (const auto log2 : {1u, 2u, 3u}) { + auto queue = makeState(); + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + queue.context[0x31c] |= 0x10000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + queue.context[0x31e] = 0x00100038u; + queue.context[0x325] = 0x1234u; + queue.context[0x3b8] = 0x4dc6c000u; + queue.context[0x2f8] = (log2 << 20u) | log2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0; + queue.context[0x2ff + pixel * 4u] = 0; + } + queue.context[0x292] = 3; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors.front().samples == (1u << log2) && state.colors.front().dccAddress == 0x123400u && state.colors.front().dccPipeAligned, "normal-render multisampled DCC state did not decode"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "normal-render multisampled DCC state failed the draw precheck"); + auto changed = queue; + changed.context[0x31e] |= 0x80u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color transforms or lossy"); + changed = queue; + changed.context[0x3b8] &= ~0x40000000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x325] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b8] |= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b0] |= 1u << 28u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips"); + } +} + void clipDistanceTests() { auto queue = makeState(); queue.context[0x207] = 0x0040000f; @@ -7839,6 +7886,11 @@ int main(int argc, char** argv) { std::cout << "Covered-centroid and multisample state checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--multisample-dcc-state") { + multisampleDccStateTests(); + std::cout << "Multisampled DCC state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 81807e14b24..d2c7fbdcc10 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -447,12 +447,212 @@ void DitheredCoverage(AgcDriver::VulkanDevice& device) { AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); } +void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto code : {0x00u, 0x40u, 0x80u, 0xc0u}) { + for (const auto alphaOnMsb : {false, true}) { + for (const auto cmaskMode : {0u, 0xccu, 0xffu}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, code, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = alphaOnMsb; + if (cmaskMode != 0) { + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + const auto expanded = samples == 2 ? 0xddu : samples == 4 ? 0xeeu : 0xffu; + std::memset(cmask.data, cmaskMode == 0xccu ? cmaskMode : expanded, BlockBytes); + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + } + const bool colorOne = code == 0x80u || code == 0xc0u; + const bool alphaOne = code == 0x40u || code == 0xc0u; + Color initial{}; + for (std::uint32_t channel = 0; channel < 4; ++channel) initial[channel] = channel == (alphaOnMsb ? 3u : 0u) ? static_cast(alphaOne) : static_cast(colorOne); + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + if (cmaskMode != 0) device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + if (cmaskMode != 0) { + auto recolored = target; + recolored.clearWords = {0x3c003c00u, 0x3c003c00u}; + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {recolored}}); + } + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = target; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? initial[channel] + (written[channel] - initial[channel]) / static_cast(samples) : initial[channel]; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); + Require(actual == expected, "multisampled DCC clear did not preserve undrawn samples, channels or scissor pixels"); + } + } + } + if (code == 0 && alphaOnMsb && cmaskMode == 0) { + static_cast(source.data)[0] = 0; + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value CPU color store did not invalidate multisampled DCC residency"); + } + if (code == 0x40u && alphaOnMsb && cmaskMode == 0) { + static_cast(metadata.data)[0] = 0x40u; + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value CPU DCC store did not invalidate multisampled DCC residency"); + } + if (code == 0xc0u && alphaOnMsb && cmaskMode == 0) { + AgcDriver::GuestMemory::Unwatch(metadata.Address(), 4096); + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("reuse requires watched guest memory") != std::string_view::npos; + } + Require(refused, "an unwatched metadata range remained eligible for multisampled DCC reuse"); + } + if (code == 0x80u && alphaOnMsb && cmaskMode == 0) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(metadata.data); + } + { + GuestAllocations::Mutation mutation; + mutation.Add(metadata.data, metadata.bytes, true, true, true); + } + bool readRefused = false; + try { + AgcDriver::Graphics::RequireNoMultisampleTarget(source.Address(), source.bytes, "a CPU read"); + } catch (const std::runtime_error&) { + readRefused = true; + } + Require(readRefused, "releasing DCC metadata removed the unpublished color read guard"); + bool reuseRefused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + reuseRefused = std::string_view(error.what()).find("metadata was released") != std::string_view::npos; + } + Require(reuseRefused, "recreated DCC metadata reused a resident color target"); + } + programCount = 0; + } + } + } + } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, mode == 0 ? 0xff : 0, metadata.bytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + std::string_view expected = "not a supported uniform fast clear"; + if (mode == 1) { + metadata.data[0] = 0x40u; + expected = "mixed multisampled DCC metadata"; + } + if (mode == 2 || mode == 3 || mode == 7) { + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + std::memset(cmask.data, mode == 2 ? 0 : 0xcc, cmask.bytes); + if (mode == 3) cmask.data[0] = 0xdd; + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + expected = "CMASK compression or register clear"; + } + if (mode == 4) { + AgcDriver::GuestMemory::Unwatch(metadata.Address(), 4096); + expected = "initial multisampled DCC contents require watched"; + } + if (mode == 5) { + target.dccAddress = source.Address(); + expected = "overlapping multisampled DCC"; + } + if (mode == 6) { + target.dccAddress += 256u; + expected = "metadata base alignment"; + } + if (mode == 7) { + target.dccControl = 0x80u; + expected = "color transform, lossy mode"; + } + bool refused = false; + try { + if (mode == 7) device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + else Draw(device, target, false, Float32Export, Program({0.25f, 0.25f, 0.25f, 0.25f}, Float32Export)); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(refused, "unsupported initial multisampled DCC contents were not refused explicitly"); + programCount = 0; + } + } +} + } int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--dcc-fast-clear") { + const auto samples = argc == 2 ? 0u : std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; + Require(argc == 2 || samples != 0, "unknown DCC test sample count"); + try { + DccFastClearTests(*device, samples); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } + std::cout << "Multisampled DCC fast clear tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--dithered8") { try { DitheredCoverage(*device); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee8be5c195d..b36be4ab30e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -43,6 +43,8 @@ ### Functional +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. + - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported.