From 0dc166e951b07dfc76ceb0e352a1c98a57c7a884 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sat, 17 Oct 2026 23:37:30 -0400 Subject: [PATCH 1/2] fix(agc): import multisampled DCC register clears --- .../prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 1 + .../prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp | 4 +++- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7cb1eafdad..11428577a2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1127,6 +1127,7 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span(dcc.front())) { + case 0x20u: return fastClearColor(target); case 0x00u: keys = DccKeys::Clear0000; break; case 0x40u: keys = DccKeys::Clear0001; break; case 0x80u: keys = DccKeys::Clear1110; break; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 8146f69adb..a9920a0307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -478,7 +478,7 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa } 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 code : {0x00u, 0x20u, 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)); @@ -491,6 +491,7 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa target.dccAddress = metadata.Address(); target.dccPipeAligned = true; target.dccAlphaOnMsb = alphaOnMsb; + if (code == 0x20u) target.clearWords = {0x38003000u, 0x3c003a00u}; if (cmaskMode != 0) { blocks.push_back(std::make_unique(BlockBytes)); auto& cmask = *blocks.back(); @@ -504,6 +505,7 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa 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); + if (code == 0x20u) initial = {0.125f, 0.5f, 0.75f, 1.0f}; 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); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6218420124..60b54282cb 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -81,7 +81,7 @@ - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache gets what the reported heap budget leaves after the heap's other use, the live sampled textures and an eighth of the budget, capped at half of the budget and of the heap, at least 512 MiB; the sampled cache reserves the larger of `TextureCacheBudget` and the storage images held, and keeps at least 2 GiB; above the floors neither budget plus the other cache's bytes exceeds the budget less its eighth. Both read the heap at most once a second and trim at their next insert, so a burst can exceed the budget. The storage budget gives back the part of the sampled floor not in use. The eighth and the floors were checked only on PPSA11386 (10 GB GPU) and PPSA16064 (8 GB GPU); the halves come from PPSA25636 on one 8 GB GPU (a 3.5-4 GiB storage working set). A title whose storage working set exceeds its budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. -- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) take only uniform 0000/0001/1110/1111 keys, or a whole-range DCC fill of one, into a single-layer, level-zero SW_64KB_R_X image of 2, 4 or 8 fragments. Other initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, partial fills, a CMASK changed without a whole fill, a range compared only after its import unless a whole fill follows, guest publication and writes over a live owner throw. Color and metadata must stay write-watched or lie in a host import the Linux probe compares, where a same-value CPU store is not seen; compared Windows imports throw. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) for GFX10.1 with 16 pipes; console geometry is unverified. +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) take uniform 0000/0001/1110/1111 keys and the register-clear key 0x20, or a whole-range DCC fill of one, into a single-layer, level-zero SW_64KB_R_X image of 2, 4 or 8 fragments. The register-clear key uses `CB_COLOR_CLEAR_WORD0/1` through the existing exact texel-to-host clear conversion; formats over 64 bits or without an exact conversion throw. Register-clear semantics follow [Mesa ac_descriptors.h](https://chromium.googlesource.com/external/gitlab.freedesktop.org/mesa/mesa/+/418c4cfa6708a0e0b1175e72fb8fd27d3ca1615a/src/amd/common/ac_descriptors.h) (`GFX8_DCC_CLEAR_REG`); the console result has not been measured. Other initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, partial fills, a CMASK changed without a whole fill, a range compared only after its import unless a whole fill follows, guest publication and writes over a live owner throw. Color and metadata must stay write-watched or lie in a host import the Linux probe compares, where a same-value CPU store is not seen; compared Windows imports throw. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) for GFX10.1 with 16 pipes; console geometry is 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. - [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color. From 3634b97d970eb6aed0dd4a961cb2ef72921be551 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 21:09:54 -0400 Subject: [PATCH 2/2] test(agc): refill resident multisampled DCC metadata with the register-clear key DccRefillTests now also refills the DCC metadata of a resident multisampled target with 0x20202020 in every accepting mode (CPU fill with and without CMASK, GPU fill under watched and unwatched imports). The next draw must clear the target to the colour of its clear registers before drawing, so the resolve equals that colour blended with the drawn sample. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/AlphaToCoverage.cpp | 176 +++++++++--------- 1 file changed, 91 insertions(+), 85 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index a9920a0307..05ec43217e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -766,102 +766,108 @@ void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSampl if (selectedSamples != 0 && samples != selectedSamples) continue; for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u}) { for (const auto cmasked : {false, true}) { - if (mode == 4 && !cmasked) continue; + for (const auto code : {0xc0u, 0x20u}) { + if (mode == 4 && !cmasked) continue; + if (code == 0x20u && (mode == 2 || mode == 3 || mode == 4)) continue; + const auto refill = code * 0x01010101u; #ifdef _WIN32 - if (mode == 6) continue; + if (mode == 6) continue; #else - AgcDriver::Graphics::SetImportWatch(context, mode == 5 ? AgcDriver::Graphics::ImportWatch::Watch : mode == 6 ? AgcDriver::Graphics::ImportWatch::Unwatch : decided); + AgcDriver::Graphics::SetImportWatch(context, mode == 5 ? AgcDriver::Graphics::ImportWatch::Watch : mode == 6 ? AgcDriver::Graphics::ImportWatch::Unwatch : decided); #endif - 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, 0, BlockBytes); - auto target = DecodeTarget(source, samples); - target.dccAddress = metadata.Address(); - target.dccPipeAligned = true; - target.dccAlphaOnMsb = true; - Block* cmask = nullptr; - if (cmasked) { + 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)); - cmask = blocks.back().get(); - std::memset(cmask->data, 0xcc, BlockBytes); - target.cmaskAddress = cmask->Address(); - target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); - target.cmaskFastClear = true; - device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); - } - Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); - const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target); - std::string_view expected; - bool rewatchable = false; - if (mode == 0) { - fill(metadata, dccBytes, 0xc0c0c0c0u); - if (cmask != nullptr) fill(*cmask, target.cmaskBytes, 0xccccccccu); - } else if (mode == 1) { - fill(metadata, dccBytes, 0xc0c0c0c0u); - } else if (mode >= 5) { - std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); - const auto record = [&](const Block& block, std::size_t bytes, std::uint32_t value) { - const std::array pattern{value, value, value, value}; - Require(device.FillBuffer(block.Address(), bytes, pattern), "the metadata fill was not recorded on the GPU"); - AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), bytes, value); - }; - record(metadata, dccBytes, 0xc0c0c0c0u); - if (cmask != nullptr) record(*cmask, target.cmaskBytes, 0xccccccccu); - const bool unwatched = !AgcDriver::GuestMemory::Watched(metadata.Address(), dccBytes) || (cmask != nullptr && !AgcDriver::GuestMemory::Watched(cmask->Address(), target.cmaskBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = true; + if (code == 0x20u) target.clearWords = {0x38003000u, 0x3c003a00u}; + const Color cleared = code == 0x20u ? Color{0.125f, 0.5f, 0.75f, 1.0f} : Color{1.0f, 1.0f, 1.0f, 1.0f}; + Block* cmask = nullptr; + if (cmasked) { + blocks.push_back(std::make_unique(BlockBytes)); + cmask = blocks.back().get(); + std::memset(cmask->data, 0xcc, BlockBytes); + target.cmaskAddress = cmask->Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + } + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target); + std::string_view expected; + bool rewatchable = false; + if (mode == 0) { + fill(metadata, dccBytes, refill); + if (cmask != nullptr) fill(*cmask, target.cmaskBytes, 0xccccccccu); + } else if (mode == 1) { + fill(metadata, dccBytes, refill); + } else if (mode >= 5) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto record = [&](const Block& block, std::size_t bytes, std::uint32_t value) { + const std::array pattern{value, value, value, value}; + Require(device.FillBuffer(block.Address(), bytes, pattern), "the metadata fill was not recorded on the GPU"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), bytes, value); + }; + record(metadata, dccBytes, refill); + if (cmask != nullptr) record(*cmask, target.cmaskBytes, 0xccccccccu); + const bool unwatched = !AgcDriver::GuestMemory::Watched(metadata.Address(), dccBytes) || (cmask != nullptr && !AgcDriver::GuestMemory::Watched(cmask->Address(), target.cmaskBytes)); #ifdef _WIN32 - if (unwatched) expected = "requires watched guest memory"; - rewatchable = unwatched; + if (unwatched) expected = "requires watched guest memory"; + rewatchable = unwatched; #else - Require(unwatched == (mode == 6), "a GPU fill of multisampled DCC metadata did not take the write-watch mode of its host import"); + Require(unwatched == (mode == 6), "a GPU fill of multisampled DCC metadata did not take the write-watch mode of its host import"); #endif - } else if (mode == 2) { - fill(metadata, dccBytes / 2u, 0xc0c0c0c0u); - expected = "writes to a resident multisampled DCC"; - } else if (mode == 3) { - fill(metadata, dccBytes, 0xffffffffu); - expected = "not a supported uniform fast clear"; - } else { - fill(*cmask, target.cmaskBytes, 0xccccccccu); - expected = "writes to a resident multisampled DCC"; - } - bool drawn = false; - if (!expected.empty()) { - bool refused = false; - try { - Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); - drawn = true; - } catch (const std::runtime_error& error) { - refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } else if (mode == 2) { + fill(metadata, dccBytes / 2u, 0xc0c0c0c0u); + expected = "writes to a resident multisampled DCC"; + } else if (mode == 3) { + fill(metadata, dccBytes, 0xffffffffu); + expected = "not a supported uniform fast clear"; + } else { + fill(*cmask, target.cmaskBytes, 0xccccccccu); + expected = "writes to a resident multisampled DCC"; } - Require(refused || (drawn && rewatchable), "a write to resident multisampled DCC metadata that is not a full fast-clear fill was not refused"); - if (refused) { - programCount = 0; - continue; - } - } - if (!drawn) Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); - 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 expectedValue = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; - const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); - Require(actual == expectedValue, "a full DCC fast-clear fill of a resident multisampled target did not clear it to the new color before the next draw"); + bool drawn = false; + if (!expected.empty()) { + bool refused = false; + try { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + drawn = true; + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(refused || (drawn && rewatchable), "a write to resident multisampled DCC metadata that is not a full fast-clear fill was not refused"); + if (refused) { + programCount = 0; + continue; } } + if (!drawn) Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + 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 expectedValue = x < ScissorWidth ? cleared[channel] + (written[channel] - cleared[channel]) / static_cast(samples) : cleared[channel]; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); + Require(actual == expectedValue, "a full DCC fast-clear fill of a resident multisampled target did not clear it to the new color before the next draw"); + } + } + } + programCount = 0; } - programCount = 0; } } }