From 1816aea91c0cbbea7a236b43ac695bc86a2e455a Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:05:37 +0200 Subject: [PATCH] fix(agc): store and read DCC keys over the console's metadata extent The console's DCC of a single-level 4-byte SW_64KB_R_X surface is ceil(W/512) * ceil(H/512) * 4096 bytes, 49152 for 1920x1080, but the driver stored and read one key per 256 surface bytes (34560). PPSA26344 retiles its render DCC into the displayable copy it registers (cs_dcc_retile_32bpp); a retile of the stale tail gives the mixed keys PPSA12544 stops on. DccKeyCount gives that extent. Color target and storage image key stores cover it when the DCC is pipe-aligned (CB_COLOR_ATTRIB3 bit 30, T# word 6 bit 19), whose CB keys reach its last meta block, and throw where it is not writable; unaligned DCC keeps one key per 256 bytes, as do texture and color target key reads. DisplayBufferKeys reads the display DCC over the extent, presents only when every key agrees, names the first differing key's pixels and checks a pending image against those keys. --- .../Execution/include/DisplayBuffer.hpp | 2 + .../Execution/src/DisplayBuffer.cpp | 65 ++++++ .../Execution/src/VulkanDevice.cpp | 27 +-- .../Graphics/include/DccMetadata.hpp | 5 + .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/include/State.hpp | 1 + .../Graphics/src/DccMetadata.cpp | 76 +++++-- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 15 +- .../Graphics/src/GuestTextureResource.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 1 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 10 +- .../tests/DisplayBufferDcc.cpp | 207 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 38 ++++ .../tests/GuestTextureResource.cpp | 25 +++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 19 +- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 2 +- docs/dev/TechnicalDebt.md | 3 +- 17 files changed, 451 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index 5c3ef004f..24a7be45b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -28,6 +28,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer); std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source); std::vector ReadDisplayBuffer(const DisplayBuffer& buffer); std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys); +std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer); +Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index 459b6e228..8bc53af7d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include namespace AgcDriver { namespace { @@ -50,6 +52,40 @@ void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBi pixel[3] = texel[3]; } +constexpr std::uint32_t MetaBlockEdge = 512; +constexpr std::size_t MetaBlockKeys = 4096; +constexpr std::uint32_t KeyEdge = 8; + +std::array displayableKeyPixel(const DisplayBuffer& buffer, std::size_t key) { + const auto pitch = (buffer.width + MetaBlockEdge - 1u) / MetaBlockEdge; + const auto block = static_cast(key / MetaBlockKeys); + const auto offset = static_cast(key % MetaBlockKeys); + std::uint32_t x = 0; + std::uint32_t y = 0; + for (std::uint32_t bit = 0; bit < 6; ++bit) { + x |= ((offset >> (2u * bit)) & 1u) << bit; + y |= ((offset >> (2u * bit + 1u)) & 1u) << bit; + } + return {block % pitch * MetaBlockEdge + x * KeyEdge, block / pitch * MetaBlockEdge + y * KeyEdge}; +} + +void traceDisplayKeys(const DisplayBuffer& buffer, const std::uint8_t* keys, std::size_t count) { + static const bool trace = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; + if (!trace) return; + constexpr std::size_t MaxRuns = 256; + std::string runs; + std::size_t begin = 0; + for (std::size_t run = 0; begin < count && run < MaxRuns; ++run) { + const auto end = static_cast(std::find_if(keys + begin, keys + count, [&](std::uint8_t key) { return key != keys[begin]; }) - keys); + char text[32]; + std::snprintf(text, sizeof(text), "%s%02x x%zu", run == 0 ? "" : ", ", keys[begin], end - begin); + runs += text; + begin = end; + } + if (begin < count) runs += ", ..."; + std::fprintf(stderr, "[dcc-keys] display keys 0x%llx+0x%zx: %s\n", static_cast(buffer.dccAddress), count, runs.c_str()); +} + } std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { @@ -117,6 +153,35 @@ std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Gr return pixel; } +std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer) { + const auto bytes = DisplayBufferSize(buffer); + require(buffer.tilingMode == 0, "VideoOut: a linear display buffer has no DCC keys"); + return Graphics::DccKeyCount(Graphics::TextureTileMode::kR64KBX, 4, buffer.width, buffer.height, bytes); +} + +Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer) { + const auto count = DisplayBufferKeyBytes(buffer); + require(buffer.dccAddress != 0, "VideoOut: a display buffer without DCC metadata has no keys"); + const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, DisplayBufferSize(buffer), count); + if (keys == Graphics::DccKeys::Uncompressed || Graphics::IsDccClear(keys)) return keys; + char differing[128] = ""; + if (keys == Graphics::DccKeys::Mixed && GuestMemory::Accessible(reinterpret_cast(buffer.dccAddress), count)) { + const auto* bytes = reinterpret_cast(buffer.dccAddress); + traceDisplayKeys(buffer, bytes, count); + const auto* other = std::find_if(bytes + 1, bytes + count, [&](std::uint8_t key) { return key != bytes[0]; }); + if (other == bytes + count) { + std::snprintf(differing, sizeof(differing), ": every key is 0x%02x", bytes[0]); + } else { + const auto key = static_cast(other - bytes); + const auto [x, y] = displayableKeyPixel(buffer, key); + std::snprintf(differing, sizeof(differing), ": key %zu (pixels %u..%u x %u..%u in the displayable order) is 0x%02x, key 0 is 0x%02x", key, x, x + KeyEdge - 1u, y, y + KeyEdge - 1u, *other, bytes[0]); + } + } + char message[384]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx over its 0x%zx key bytes%s: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast(buffer.address), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress), count, differing); + throw std::runtime_error(message); +} + std::vector ReadDisplayBuffer(const DisplayBuffer& buffer) { const auto size = DisplayBufferSize(buffer); static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 09ea44fae..3fb1d8798 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1950,37 +1950,30 @@ std::shared_ptr PresentableResident(const Graphics::Co return resident; } -Graphics::DccKeys DisplayKeys(const DisplayBuffer& buffer, std::size_t bytes) { - const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, bytes); - if (keys != Graphics::DccKeys::Uncompressed && !Graphics::IsDccClear(keys)) { - char message[256]; - std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast(buffer.address), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress)); - throw std::runtime_error(message); - } - return keys; +Graphics::DccKeys ResidentKeys(const Graphics::StorageTexture& resident) { + const auto& own = resident.Descriptor(); + GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes())); + return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof()); } -bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer, std::size_t bytes) { - const auto keys = DisplayKeys(buffer, bytes); +bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer) { + const auto keys = DisplayBufferKeys(buffer); if (keys == Graphics::DccKeys::ClearRegister) { char message[320]; std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads register-clear DCC keys at 0x%llx over the pending image 0x%llx (DCC 0x%llx, filled keys %s): whether its results precede the clear is not modeled", static_cast(buffer.address), static_cast(buffer.dccAddress), static_cast(resident.Descriptor().baseAddress), static_cast(resident.Descriptor().dccAddress), Graphics::DccKeysName(resident.FilledKeys())); throw std::runtime_error(message); } if (Graphics::IsDccClear(resident.FilledKeys()) && resident.FilledKeys() == keys) return false; - return Graphics::StorageImageServesKeys(resident, buffer.dccAddress); + return Graphics::KeysServeSurface(resident.Descriptor().dccAddress, resident.UploadedKeys(), resident.FilledKeys(), buffer.dccAddress, [&] { return ResidentKeys(resident); }, [&] { return keys; }); } bool ResidentKeysMoved(const Graphics::StorageTexture& resident) { - const auto& own = resident.Descriptor(); - if (own.dccAddress == 0) return false; - GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes())); - return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof()) != resident.UploadedKeys(); + return resident.Descriptor().dccAddress != 0 && ResidentKeys(resident) != resident.UploadedKeys(); } std::optional> CompressedClearPixel(const DisplayBuffer& buffer, std::size_t bytes) { GuestMemory::FlushGpuWrites(buffer.address, bytes); - const auto keys = DisplayKeys(buffer, bytes); + const auto keys = DisplayBufferKeys(buffer); if (keys == Graphics::DccKeys::Uncompressed) return std::nullopt; return DisplayBufferClearPixel(buffer, keys); } @@ -2034,7 +2027,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (!NoResidentPresent()) { bool pending = false; resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); - if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer, bytes)) { + if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer)) { resident.reset(); convert = false; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index 8808fb712..109a02b39 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -23,6 +23,8 @@ enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, C const char* DccKeysName(DccKeys keys); std::size_t DccKeyBytes(std::uint64_t surfaceBytes); +std::size_t DccKeyCount(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t width, std::uint32_t height, std::uint64_t surfaceBytes); +std::size_t DccKeyCount(const GuestTextureResource& surface, std::uint64_t surfaceBytes); template bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys filled, std::uint64_t namedDcc, ReadFollowed&& readFollowed, ReadNamed&& readNamed) { @@ -35,15 +37,18 @@ bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys fille DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); bool IsDccClear(DccKeys keys); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); +DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount); // Stores "uncompressed" keys over the surface's metadata on the CPU (a guest memory write: it waits // for recorded GPU work that writes the keys first). void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes); +void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount); // The same after a write-back recorded under GuestMemory::GpuMutex: when the keys are in host-imported // memory and a recorder is active, the store is a fill recorded into the open batch (ordered behind // the title's key-writing kernels like the write-back itself, nothing waits on the CPU) and the range // reads as uncompressed from ReadDccKeys while that batch is pending; otherwise the CPU store above. // APS5_CPU_DCC_KEYS=1 always stores on the CPU. void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes); +void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount); // Fills `bytes` with the texel a 0000/0001/1110/1111 clear code stands for ("1" is 1.0 or the integer // maximum; the alpha channel is the last one in memory when alphaOnMsb, else the first, and 3-channel // formats have none). False when the format has no encoding here. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 6397574f3..aa0765d12 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -81,6 +81,7 @@ struct GuestTextureResource { // DCC metadata of a compressed surface, or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; + bool dccPipeAligned = false; std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index e54f660f7..123dbe52a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -41,6 +41,7 @@ struct ColorTarget { // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; + bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; std::size_t cmaskBytes = 0; std::uint64_t surfaceAddress = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index c6deb8e28..5e4729b1f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -392,13 +392,27 @@ std::size_t DccKeyBytes(std::uint64_t surfaceBytes) { return static_cast(surfaceBytes / KeyBytes); } +std::size_t DccKeyCount(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t width, std::uint32_t height, std::uint64_t surfaceBytes) { + const auto keys = DccKeyBytes(surfaceBytes); + if (tileMode != TextureTileMode::kR64KBX || elementBytes != 4) return keys; + constexpr std::uint64_t MetaBlockEdge = 512; + constexpr std::uint64_t MetaBlockBytes = 4096; + const auto console = (width + MetaBlockEdge - 1) / MetaBlockEdge * ((height + MetaBlockEdge - 1) / MetaBlockEdge) * MetaBlockBytes; + return std::max(keys, static_cast(console)); +} + +std::size_t DccKeyCount(const GuestTextureResource& surface, std::uint64_t surfaceBytes) { + const bool single = surface.mipCount == 1 && surface.baseArray == 0 && surface.depthOrLastArray == 0 && (surface.dimension == TextureDimension::k2D || surface.dimension == TextureDimension::k2DArray); + if (!single || !surface.dccPipeAligned || surface.tileMode != TextureTileMode::kR64KBX) return DccKeyBytes(surfaceBytes); + return DccKeyCount(surface.tileMode, BytesPerElement(surface.format), surface.width, surface.height, surfaceBytes); +} + namespace { // ReadDccKeys, saying in `memoized` whether the answer came from the pending-store memo rather // than the bytes. -DccKeys readDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, bool& memoized) { +DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized) { memoized = false; - const auto count = static_cast(surfaceBytes / KeyBytes); if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast(metaAddress), count)) return DccKeys::Unreadable; // A driver store still pending on the GPU is not in the bytes yet. if (const auto memo = MemoizedKeys(metaAddress, metaAddress + count)) { @@ -430,7 +444,7 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, bool& DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) { memoized = false; if (resource.dccAddress == 0) return DccKeys::Uncompressed; - const auto keys = readDccKeys(resource.dccAddress, guestBytes, memoized); + const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); if (keys == DccKeys::Uncompressed) return keys; if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); std::byte probe[16]{}; @@ -444,15 +458,7 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue return keys; } -} - -DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { - bool memoized = false; - return readDccKeys(metaAddress, surfaceBytes, memoized); -} - -DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { - const auto count = static_cast(surfaceBytes / KeyBytes); +DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count) { if (metaAddress != 0 && count != 0 && GuestMemory::GpuMutex().HeldByThisThread()) { if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count)) { if (const auto keys = PendingStoreKeys(*recorder, metaAddress, count)) return *keys; @@ -460,7 +466,35 @@ DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { recorder->SyncThrough(metaAddress, count); } } - return ReadDccKeys(metaAddress, surfaceBytes); + bool memoized = false; + return readDccKeys(metaAddress, count, memoized); +} + +std::size_t StoredKeyCount(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) { + const auto keys = DccKeyBytes(surfaceBytes); + Require(keyCount >= keys, "a DCC key range covers fewer keys than one per 256 surface bytes"); + if (metaAddress == 0 || keyCount == 0) return 0; + if (GuestMemory::Accessible(reinterpret_cast(metaAddress), keyCount, true)) return keyCount; + if (keyCount == keys) return 0; + char message[256]; + std::snprintf(message, sizeof(message), "AGC graphics: DCC metadata 0x%llx is not writable over the 0x%zx key bytes of its surface (0x%zx at one per 256 bytes): metadata smaller than the console's DCC extent is not modeled", static_cast(metaAddress), keyCount, keys); + throw std::runtime_error(message); +} + +} + +DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { + bool memoized = false; + return readDccKeys(metaAddress, DccKeyBytes(surfaceBytes), memoized); +} + +DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { + return currentDccKeys(metaAddress, DccKeyBytes(surfaceBytes)); +} + +DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) { + Require(keyCount >= DccKeyBytes(surfaceBytes), "a DCC key range covers fewer keys than one per 256 surface bytes"); + return currentDccKeys(metaAddress, keyCount); } bool IsDccClear(DccKeys keys) { @@ -468,8 +502,12 @@ bool IsDccClear(DccKeys keys) { } void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { - const auto count = static_cast(surfaceBytes / KeyBytes); - if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast(metaAddress), count, true)) return; + MarkDccUncompressed(metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes)); +} + +void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) { + const auto count = StoredKeyCount(metaAddress, surfaceBytes, keyCount); + if (count == 0) return; // The bytes are trusted only when no recorded work writes them (lock-free, with or without // GuestMemory::GpuMutex); otherwise the store runs and waits through the flush hook. A pending // GPU store of the driver's own is no reason to skip: a title kernel recorded into the same @@ -479,8 +517,12 @@ void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) } void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes) { - const auto count = static_cast(surfaceBytes / KeyBytes); - if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast(metaAddress), count, true)) return; + MarkDccUncompressed(context, metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes)); +} + +void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) { + const auto count = StoredKeyCount(metaAddress, surfaceBytes, keyCount); + if (count == 0) return; auto* recorder = CpuKeysOnly() || !GuestMemory::GpuMutex().HeldByThisThread() ? nullptr : Recorder::Active(); // The bytes are scanned only when no recorded work writes them: a pending clear kernel's keys are // not in memory yet, so the bytes could read as uncompressed while the kernel will store a clear diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 6a821c3d0..646daec81 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -62,6 +62,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { surface.dstSelW = 7; surface.dccAddress = color.dccAddress; surface.dccAlphaOnMsb = color.dccAlphaOnMsb; + surface.dccPipeAligned = color.dccPipeAligned; return surface; } @@ -69,6 +70,11 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { namespace { +std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) { + if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes); + return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes); +} + std::array clearTexel(const ColorTarget& color, DccKeys keys) { std::array texel{}; const auto elementBytes = static_cast(color.elementBytes); @@ -102,7 +108,7 @@ void storeClearTexels(const Context& context, const ColorTarget& color, const st const auto keys = ReadDccKeys(color.dccAddress, color.bytes); if (!IsDccClear(keys)) return; writeTexels(color, keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys)); - MarkDccUncompressed(context, color.dccAddress, color.bytes); + MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); } void materializeRegisterClear(const Context& context, const ColorTarget& color, StorageTexture& resident) { @@ -121,7 +127,7 @@ void materializeRegisterClear(const Context& context, const ColorTarget& color, } } if (cleared) { - MarkDccUncompressed(context, color.dccAddress, color.bytes); + MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); return; } storeClearTexels(context, color, texel); @@ -1990,7 +1996,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original); else WriteColorTarget(binding.color, binding.transfer->Bytes()); // The stored texels are the whole target now, so later reads must see them rather than a fast clear. - MarkDccUncompressed(binding.color.dccAddress, ColorTargetLayout(binding.color.extent.width, binding.color.extent.height, binding.color.tileMode, binding.color.elementBytes).Bytes()); + const auto targetBytes = ColorTargetLayout(binding.color.extent.width, binding.color.extent.height, binding.color.tileMode, binding.color.elementBytes).Bytes(); + MarkDccUncompressed(binding.color.dccAddress, targetBytes, colorKeyCount(binding.color, targetBytes)); } timer.phase(PhaseWriteBack); if (profile && traceDraws) { @@ -2159,7 +2166,7 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; if (current) { resident->MarkDirty(); - MarkDccUncompressed(context, color.dccAddress, color.bytes); + MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); continue; } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 9875b4d56..f97d82c06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -115,7 +115,6 @@ GuestTextureResource DecodeTextureResource(std::span words) static_cast(maxCompBlkSize); // Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so // only the fast-clear keys in DCC metadata change what a read returns (see DccMetadata.hpp). - static_cast(metaPipeAligned); static_cast(writeCompress); if (dccColorTransf) { static bool reported = false; @@ -193,6 +192,7 @@ GuestTextureResource DecodeTextureResource(std::span words) result.dstSelW = static_cast(dstSelW); result.dccAddress = metaCompress ? metaAddr << 8u : 0u; result.dccAlphaOnMsb = dccAlphaPos; + result.dccPipeAligned = metaPipeAligned; result.minLod = minLod; if (baseLevel > maxMip) { Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index e16378102..dc0ab6fb9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -772,6 +772,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto dccHigh = find(cx, 0x3a8 + slot); 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; } else { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ccd60a9b0..86c3338eb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1472,7 +1472,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std static const bool trace = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; if (!trace || descriptor.dccAddress == 0) return; const auto packet = GuestMemory::CurrentPacket(); - std::fprintf(stderr, "[dcc-keys] uncompressed store by %s (%s) for surface 0x%llx+0x%llx: keys 0x%llx+0x%llx (packet 0x%x queue 0x%x)\n", path, flushReason != nullptr ? flushReason : "?", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(descriptor.dccAddress), static_cast(guestBytes / 256), packet.opcode, packet.queue); + std::fprintf(stderr, "[dcc-keys] uncompressed store by %s (%s) for surface 0x%llx+0x%llx: keys 0x%llx+0x%llx (packet 0x%x queue 0x%x)\n", path, flushReason != nullptr ? flushReason : "?", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(descriptor.dccAddress), static_cast(DccKeyCount(descriptor, guestBytes)), packet.opcode, packet.queue); } } @@ -2215,7 +2215,7 @@ bool StorageTexture::overlaps(std::uint64_t address, std::size_t bytes) const { void StorageTexture::MarkDirty() { if (descriptor.dccAddress != 0 && IsDccClear(uploadedKeys) && !IsDccClear(filledKeys)) { traceKeyStore("first write", descriptor, guestBytes); - MarkDccUncompressed(context, descriptor.dccAddress, guestBytes); + MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; } markLayersPending(0, trackedLayers); @@ -3485,7 +3485,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { if (profile) Profile().storageGpu += timer.lap(); originalValid = false; traceKeyStore("block write-back", descriptor, guestBytes); - if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes); + if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast(to - from)); settle(true); @@ -3593,7 +3593,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // metadata is host-imported (no CPU wait for the title's key-writing kernels), else a CPU // store (APS5_CPU_DCC_KEYS=1 keeps the CPU store; see DccMetadata.hpp). traceKeyStore("layer write-back", descriptor, guestBytes); - if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes); + if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast(to - from)); // The walk covers this surface's pages only: an adjacent image's later CPU write is stamped @@ -3703,7 +3703,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // The texels now hold the whole image, so later reads must see them rather than a fast clear // (the keys may be host-imported although the texels were not: then a recorded fill, else a CPU store). traceKeyStore("cpu write-back", descriptor, guestBytes); - if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes); + if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; // The store above is the only write to these pages, so `original` is current at a fresh // generation, unless blocks were kept for the CPU: then the image is stale there. diff --git a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp index b2173b7ec..f5f1f159f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp @@ -1,13 +1,30 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include #include +#include #include #include +#include #include #include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif using AgcDriver::DisplayBuffer; using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::GuestTextureResource; +using AgcDriver::Graphics::TextureDimension; +using AgcDriver::Graphics::TextureTileMode; namespace { @@ -38,6 +55,193 @@ void ExpectPixels(const DisplayBuffer& buffer, DccKeys keys, std::array; +constexpr DccPattern PipeAligned{X(3), Y(4), X(6), Y(6), X(7), Y(7), X(8), Y(8), X(3) ^ Y(3), X(4) ^ Y(4), X(6) ^ Y(5), X(5) ^ Y(6)}; +constexpr DccPattern Displayable{X(3), Y(3), X(4), Y(4), X(5), Y(5), X(6), Y(6), X(7), Y(7), X(8), Y(8)}; + +std::size_t PatternKey(const DccPattern& pattern, std::uint32_t width, std::uint32_t x, std::uint32_t y) { + std::size_t offset = 0; + for (std::size_t bit = 0; bit < pattern.size(); ++bit) offset |= static_cast((std::popcount(x & pattern[bit].x) + std::popcount(y & pattern[bit].y)) & 1) << bit; + return (static_cast(y / 512u) * ((width + 511u) / 512u) + x / 512u) * 4096u + offset; +} + +void Retile(std::span render, std::span display, std::uint32_t width, std::uint32_t height) { + for (std::uint32_t y = 0; y < (height + 511u) / 512u * 512u; y += 8) { + for (std::uint32_t x = 0; x < (width + 511u) / 512u * 512u; x += 8) display[PatternKey(Displayable, width, x, y)] = render[PatternKey(PipeAligned, width, x, y)]; + } +} + +struct KeyHistogram { + std::size_t uncompressed = 0; + std::size_t clear0000 = 0; + std::size_t firstDiffering = 0; + std::size_t zeroRuns = 0; + bool operator==(const KeyHistogram&) const = default; +}; + +KeyHistogram Histogram(std::span keys) { + KeyHistogram histogram; + for (std::size_t key = 0; key < keys.size(); ++key) { + histogram.uncompressed += keys[key] == 0xff; + histogram.clear0000 += keys[key] == 0x00; + histogram.zeroRuns += keys[key] == 0x00 && (key == 0 || keys[key - 1] != 0x00); + if (histogram.firstDiffering == 0 && keys[key] != keys[0]) histogram.firstDiffering = key; + } + return histogram; +} + +std::string Describe(const KeyHistogram& histogram) { + return std::to_string(histogram.uncompressed) + " x 0xff, " + std::to_string(histogram.clear0000) + " x 0x00, first differing key " + std::to_string(histogram.firstDiffering) + ", " + std::to_string(histogram.zeroRuns) + " runs of 0x00"; +} + +std::uint64_t TiledBytes(std::uint32_t width, std::uint32_t height) { + return static_cast((width + 127u) / 128u) * ((height + 127u) / 128u) * 65536u; +} + +GuestTextureResource Surface(std::uint32_t width, std::uint32_t height, std::uint64_t dccAddress, bool pipeAligned) { + GuestTextureResource surface{}; + surface.baseAddress = 0x10000; + surface.width = width; + surface.height = height; + surface.mipCount = 1; + surface.tileMode = TextureTileMode::kR64KBX; + surface.dimension = TextureDimension::k2D; + surface.format = AgcDriver::Graphics::FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_UNORM, 4).value(); + surface.dccAddress = dccAddress; + surface.dccPipeAligned = pipeAligned; + return surface; +} + +std::uint8_t* ReserveKeys(std::size_t writable, std::size_t reserved) { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, reserved, MEM_RESERVE, PAGE_NOACCESS)); + if (block == nullptr || VirtualAlloc(block, writable, MEM_COMMIT, PAGE_READWRITE) == nullptr) return nullptr; +#else + void* mapped = mmap(nullptr, reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped == MAP_FAILED || mprotect(mapped, writable, PROT_READ | PROT_WRITE) != 0) return nullptr; + auto* block = static_cast(mapped); +#endif + return block; +} + +void ReleaseKeys(std::uint8_t* block, std::size_t reserved) { +#ifdef _WIN32 + static_cast(reserved); + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, reserved); +#endif +} + +void KeyCountTests() { + struct Extent { + std::uint32_t width; + std::uint32_t height; + std::size_t keys; + }; + for (const auto& extent : {Extent{1920, 1080, 49152}, Extent{3840, 2160, 163840}, Extent{1280, 720, 24576}, Extent{259, 137, 4096}, Extent{128, 128, 4096}, Extent{2560, 1440, 61440}}) { + const auto surfaceBytes = TiledBytes(extent.width, extent.height); + const auto name = std::to_string(extent.width) + "x" + std::to_string(extent.height); + const auto keys = AgcDriver::Graphics::DccKeyCount(TextureTileMode::kR64KBX, 4, extent.width, extent.height, surfaceBytes); + Expect(keys == extent.keys, name + " SW_64KB_R_X 4-byte DCC covers " + std::to_string(keys) + " key bytes, expected " + std::to_string(extent.keys)); + if (extent.keys == surfaceBytes / 256) continue; + const auto aligned = AgcDriver::Graphics::DccKeyCount(Surface(extent.width, extent.height, 0, true), surfaceBytes); + const auto unaligned = AgcDriver::Graphics::DccKeyCount(Surface(extent.width, extent.height, 0, false), surfaceBytes); + Expect(aligned == extent.keys && unaligned == surfaceBytes / 256, name + " surface descriptors' DCC covers " + std::to_string(aligned) + " key bytes pipe-aligned and " + std::to_string(unaligned) + " unaligned, expected " + std::to_string(extent.keys) + " and " + std::to_string(surfaceBytes / 256)); + } + Expect(TiledBytes(2560, 1440) / 256 == 61440, "2560x1440 does not fill its meta blocks"); + const auto bytes = TiledBytes(1920, 1080); + Expect(AgcDriver::Graphics::DccKeyCount(TextureTileMode::kD64KBX, 4, 1920, 1080, bytes) == bytes / 256, "an SW_64KB_D_X surface's DCC left one key per 256 bytes"); + Expect(AgcDriver::Graphics::DccKeyCount(TextureTileMode::kR64KBX, 8, 1920, 1080, 2 * bytes) == 2 * bytes / 256, "an 8-byte surface's DCC left one key per 256 bytes"); + const auto surface = Surface(1920, 1080, 0, true); + auto shape = surface; + shape.mipCount = 2; + Expect(AgcDriver::Graphics::DccKeyCount(shape, bytes + 65536) == (bytes + 65536) / 256, "a mip chain's DCC left one key per 256 bytes"); + shape = surface; + shape.dimension = TextureDimension::k2DArray; + shape.depthOrLastArray = 1; + Expect(AgcDriver::Graphics::DccKeyCount(shape, 2 * bytes) == 2 * bytes / 256, "a two-layer surface's DCC left one key per 256 bytes"); + shape = surface; + shape.format = AgcDriver::Graphics::FindGuestTextureFormat(VK_FORMAT_R16G16B16A16_SFLOAT, 8).value(); + Expect(AgcDriver::Graphics::DccKeyCount(shape, 2 * bytes) == 2 * bytes / 256, "an 8-byte surface descriptor's DCC left one key per 256 bytes"); + Expect(AgcDriver::DisplayBufferKeyBytes({65536, 0x8000000000000000ull, 1920, 1080}) == 49152 && AgcDriver::DisplayBufferKeyBytes({65536, 0x8000000000000000ull, 259, 137}) == 4096, "display buffers do not read the console's DCC extent"); +} + +void RetileTests() { + constexpr std::uint32_t width = 1920; + constexpr std::uint32_t height = 1080; + const auto surfaceBytes = TiledBytes(width, height); + const auto driverKeys = static_cast(surfaceBytes / 256); + std::vector render(49152); + std::vector display(render.size()); + std::fill_n(render.begin(), driverKeys, 0xff); + Retile(render, display, width, height); + const auto reported = Histogram(std::span(display).first(driverKeys)); + Expect(reported == KeyHistogram{33552, 1008, 32784, 105}, "a retile of 0xff over the first 34560 render keys does not give the PPSA12544 display keys: " + Describe(reported)); + std::fill(render.begin(), render.end(), 0x00); + std::fill(display.begin(), display.end(), 0x00); + const auto surface = Surface(width, height, reinterpret_cast(render.data()), true); + AgcDriver::Graphics::MarkDccUncompressed(surface.dccAddress, surfaceBytes, AgcDriver::Graphics::DccKeyCount(surface, surfaceBytes)); + Retile(render, display, width, height); + DisplayBuffer buffer{65536, 0x8000000000000000ull, width, height}; + buffer.dccAddress = reinterpret_cast(display.data()); + auto keys = DccKeys::Unreadable; + const auto message = Rejection([&] { keys = AgcDriver::DisplayBufferKeys(buffer); }); + Expect(message.empty() && keys == DccKeys::Uncompressed, "the driver's store over a 1920x1080 render DCC, retiled, does not present: " + message + " (display keys over the first " + std::to_string(driverKeys) + ": " + Describe(Histogram(std::span(display).first(driverKeys))) + ")"); + + constexpr std::size_t reserved = 65536; + constexpr std::size_t writable = 0x9000; + auto* small = ReserveKeys(writable, reserved); + Expect(small != nullptr, "cannot reserve metadata with an inaccessible tail"); + if (small == nullptr) return; + const auto stored = Rejection([&] { AgcDriver::Graphics::MarkDccUncompressed(reinterpret_cast(small), surfaceBytes, AgcDriver::Graphics::DccKeyCount(surface, surfaceBytes)); }); + Expect(stored.find("not writable over the 0xc000 key bytes") != std::string::npos && small[0] == 0x00, "a key store over metadata smaller than the console's extent did not throw: " + stored); + const auto unaligned = Rejection([&] { AgcDriver::Graphics::MarkDccUncompressed(reinterpret_cast(small), surfaceBytes, AgcDriver::Graphics::DccKeyCount(Surface(width, height, reinterpret_cast(small), false), surfaceBytes)); }); + Expect(unaligned.empty() && std::all_of(small, small + driverKeys, [](std::uint8_t key) { return key == 0xff; }) && small[driverKeys] == 0x00, "an unaligned DCC key store over metadata smaller than the console's extent did not cover one key per 256 bytes: " + unaligned); + ReleaseKeys(small, reserved); +} + +DccKeys ReadKeys(DisplayBuffer buffer, std::vector& keys) { + buffer.dccAddress = reinterpret_cast(keys.data()); + return AgcDriver::DisplayBufferKeys(buffer); +} + +void ExtentTests() { + const DisplayBuffer full{65536, 0x8000000000000000ull, 1920, 1080}; + std::vector keys(49152); + for (const auto& [code, expected] : {std::pair{std::uint8_t{0xff}, DccKeys::Uncompressed}, std::pair{std::uint8_t{0x00}, DccKeys::Clear0000}, std::pair{std::uint8_t{0xc0}, DccKeys::Clear1111}, std::pair{std::uint8_t{0x20}, DccKeys::ClearRegister}}) { + std::fill(keys.begin(), keys.end(), code); + auto read = DccKeys::Unreadable; + const auto message = Rejection([&] { read = ReadKeys(full, keys); }); + Expect(message.empty() && read == expected, std::string("uniform ") + AgcDriver::Graphics::DccKeysName(expected) + " keys over the extent read " + AgcDriver::Graphics::DccKeysName(read) + ": " + message); + } + std::fill(keys.begin(), keys.end(), 0xff); + const auto corner = PatternKey(Displayable, 1920, 1919, 1079); + Expect(corner == 46205, "pixel (1919, 1079) is not displayable key 46205"); + keys[corner] = 0x00; + auto message = Rejection([&] { ReadKeys(full, keys); }); + Expect(message.find("mixed") != std::string::npos && message.find("key 46205 (pixels 1912..1919 x 1072..1079 in the displayable order) is 0x00, key 0 is 0xff") != std::string::npos, "a 0000 key past one key per 256 bytes was not read or not named: " + message); + std::fill(keys.begin(), keys.end(), 0x40); + keys[0] = 0x80; + message = Rejection([&] { ReadKeys(full, keys); }); + Expect(message.find("key 1 (pixels 8..15 x 0..7 in the displayable order) is 0x40, key 0 is 0x80") != std::string::npos, "a 1110 key 0 among 0001 keys was not named: " + message); + const DisplayBuffer odd{65536, 0x8000000000000000ull, 259, 137}; + keys.assign(4096, 0x40); + keys.back() = 0xff; + message = Rejection([&] { ReadKeys(odd, keys); }); + Expect(message.find("key 4095 (pixels 504..511 x 504..511 in the displayable order) is 0xff") != std::string::npos, "259x137: an uncompressed padding key among 0001 keys was ignored: " + message); + Expect(!Rejection([&] { AgcDriver::DisplayBufferKeys(full); }).empty(), "a display buffer without DCC metadata had keys read"); +} + } int main() { @@ -77,6 +281,9 @@ int main() { Expect(!Rejection([&] { AgcDriver::DisplayBufferSize(buffer); }).empty(), "a linear display buffer with DCC metadata was accepted"); buffer.tilingMode = 0; Expect(AgcDriver::DisplayBufferSize(buffer) == 2 * 65536, "DCC metadata changed the display footprint"); + KeyCountTests(); + RetileTests(); + ExtentTests(); if (failures != 0) return 1; std::puts("display buffer DCC tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 547221197..220231386 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -983,6 +984,43 @@ void metadataPassTests() { Require(AgcDriver::Graphics::FillDccClear(tenBitPass->targets[0].format, clear.keys, tenBitPass->targets[0].dccAlphaOnMsb, std::as_writable_bytes(std::span(copied))) && std::ranges::all_of(copied, [&](std::uint32_t word) { return word == clear.texel; }), "a copied " + name + " target under " + clear.code + " keys was not filled with its 10/10/10/2 texel"); } } + + constexpr std::uint32_t tiledSide = 128; + constexpr std::size_t tiledBytes = tiledSide * tiledSide * 4; + constexpr std::size_t blockAlignment = 65536; + const auto keyCount = AgcDriver::Graphics::DccKeyCount(AgcDriver::Graphics::TextureTileMode::kR64KBX, 4, tiledSide, tiledSide, tiledBytes); + Require(keyCount == 4096 && AgcDriver::Graphics::DccKeyBytes(tiledBytes) == 256, "the 128x128 SW_64KB_R_X DCC extent changed"); + std::vector tiledBlock(blockAlignment + tiledBytes + keyCount); + const auto tiledAddress = (reinterpret_cast(tiledBlock.data()) + blockAlignment - 1) / blockAlignment * blockAlignment; + auto* tiledTexels = reinterpret_cast(tiledAddress); + auto* tiledKeys = tiledTexels + tiledBytes; + for (const bool pipeAligned : {true, false}) { + auto tiled = queue; + tiled.context[0x3b8] |= (static_cast(AgcDriver::Graphics::ColorTileMode::RenderTarget) << 14u) | (pipeAligned ? 1u << 30u : 0u); + tiled.context[0x3b0] = ((tiledSide - 1u) << 14u) | (tiledSide - 1u); + tiled.context[0x318] = static_cast(tiledAddress >> 8u); + tiled.context[0x390] = static_cast(tiledAddress >> 40u); + tiled.context[0x325] = static_cast((tiledAddress + tiledBytes) >> 8u); + tiled.context[0x3a8] = static_cast((tiledAddress + tiledBytes) >> 40u); + for (const auto offset : {0xdu, 0x82u, 0x91u, 0x95u}) tiled.context[offset] = (tiledSide << 16u) | tiledSide; + tiled.context[0x10f] = std::bit_cast(64.0f); + tiled.context[0x110] = std::bit_cast(64.0f); + tiled.context[0x111] = std::bit_cast(-64.0f); + tiled.context[0x112] = std::bit_cast(64.0f); + const auto tiledPass = DecodeColorMetadataPass(tiled); + const std::string alignment = pipeAligned ? "pipe-aligned" : "unaligned"; + Require(tiledPass.has_value() && tiledPass->targets.size() == 1 && tiledPass->targets[0].tileMode == AgcDriver::Graphics::ColorTileMode::RenderTarget && tiledPass->targets[0].bytes == tiledBytes && tiledPass->targets[0].dccAddress == tiledAddress + tiledBytes && tiledPass->targets[0].dccPipeAligned == pipeAligned, "the 128x128 SW_64KB_R_X metadata pass target with " + alignment + " DCC changed"); + const auto expected = pipeAligned ? keyCount : AgcDriver::Graphics::DccKeyBytes(tiledBytes); + for (const auto& [key, texel, code] : {std::tuple{std::uint8_t{0x20}, 0x11223344u, "register"}, std::tuple{std::uint8_t{0xc0}, 0xffffffffu, "1111"}}) { + std::memset(tiledTexels, 0x5a, tiledBytes); + std::memset(tiledKeys, key, keyCount); + AgcDriver::Graphics::RunColorMetadataPass(context, *tiledPass); + const auto stored = static_cast(std::count(tiledKeys, tiledKeys + keyCount, std::uint8_t{0xff})); + bool filled = true; + for (std::size_t offset = 0; offset < tiledBytes; offset += 4) filled = filled && std::memcmp(tiledTexels + offset, &texel, 4) == 0; + Require(filled && stored == expected && std::all_of(tiledKeys, tiledKeys + expected, [](std::uint8_t value) { return value == 0xff; }), std::string("a ") + code + " fast clear eliminate of a 128x128 SW_64KB_R_X target with " + alignment + " DCC stored uncompressed keys over " + std::to_string(stored) + " of its " + std::to_string(keyCount) + " DCC key bytes, expected " + std::to_string(expected)); + } + } } alignas(256) std::array cmaskMemory{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 7e0252e71..44b30348e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -1,10 +1,13 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include #include #include #include #include +#include namespace { @@ -301,6 +304,28 @@ void RunGuestTextureResourceTests() { const auto compressed = DecodeTextureResource(pack(meta)); Require(compressed.dccAddress == 0x100 && compressed.dccAlphaOnMsb, "DCC metadata was decoded wrongly"); + Fields storage = base; + storage.base40 = 0x1000; + storage.width = 1920; + storage.height = 1080; + storage.tileModeRaw = 27; + storage.metaCompress = true; + constexpr std::uint64_t storageBytes = 15u * 9u * 65536u; + constexpr std::size_t extentKeys = 49152; + std::vector storageKeys(extentKeys + 256); + const auto keysAddress = (reinterpret_cast(storageKeys.data()) + 255u) / 256u * 256u; + const auto* keys = reinterpret_cast(keysAddress); + storage.metaAddr = keysAddress >> 8u; + for (const bool pipeAligned : {true, false}) { + storage.metaPipeAligned = pipeAligned; + const auto image = DecodeTextureResource(pack(storage)); + const auto expected = pipeAligned ? extentKeys : static_cast(storageBytes / 256u); + std::fill(storageKeys.begin(), storageKeys.end(), std::uint8_t{0x00}); + MarkDccUncompressed(image.dccAddress, storageBytes, DccKeyCount(image, storageBytes)); + const auto stored = static_cast(std::count(keys, keys + extentKeys, std::uint8_t{0xff})); + Require(image.dccPipeAligned == pipeAligned && stored == expected && std::all_of(keys, keys + expected, [](std::uint8_t key) { return key == 0xff; }), std::string("a 1920x1080 SW_64KB_R_X storage image with ") + (pipeAligned ? "pipe-aligned" : "unaligned") + " DCC stored " + std::to_string(stored) + " uncompressed keys, expected " + std::to_string(expected)); + } + Fields badSwizzle = base; badSwizzle.bcSwizzle = 1; rejectFields(badSwizzle, "BC swizzle"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a7d691a7a..196642f7d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2219,6 +2219,18 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { recorder.NotePendingWrite(address, 2 * keyCount); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later wider writer over a pending fill was not waited for"); recorder.Sync(); + constexpr std::uint64_t displayBytes = 15u * 9u * 65536u; + constexpr std::size_t consoleKeys = 49152; + const auto displayKeys = DccKeyCount(TextureTileMode::kR64KBX, 4, 1920, 1080, displayBytes); + std::memset(keys, 0x00, consoleKeys); + MarkDccUncompressed(context, address, displayBytes, displayKeys); + Require(recorder.PendingWriteOverlaps(address, consoleKeys) && keys[consoleKeys - 1] == 0x00, "the uncompressed key store over a 1920x1080 surface did not stay pending"); + Require(CurrentDccKeys(address + displayBytes / 256, (consoleKeys - displayBytes / 256) * 256) == DccKeys::Uncompressed && recorder.PendingWriteOverlaps(address, consoleKeys), "the keys past one per 256 bytes of a pending 1920x1080 store did not read as uncompressed without a wait"); + Require(CurrentDccKeys(address, displayBytes, displayKeys) == DccKeys::Uncompressed && recorder.PendingWriteOverlaps(address, consoleKeys), "the keys of a pending 1920x1080 store did not read as uncompressed over the console's extent without a wait"); + recorder.NotePendingWrite(address + 40000, 1); + Require(CurrentDccKeys(address, displayBytes, displayKeys) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address + 40000, 1), "a later writer of key 40000 over a pending 1920x1080 store was not waited for"); + Require(std::all_of(keys, keys + consoleKeys, [](std::uint8_t key) { return key == 0xff; }), "the uncompressed key store over a 1920x1080 surface did not land on all 49152 keys"); + recorder.Sync(); } void sampleDumpTests(const Device& device, Recorder& recorder) { @@ -2335,6 +2347,7 @@ void metadataPassTests(const Device& device, Recorder& recorder) { color.tileMode = ColorTileMode::RenderTarget; color.elementBytes = 4; color.dccAddress = address + surfaceBytes; + color.dccPipeAligned = true; color.clearWords = {0x80402010u, 0}; const ColorMetadataPass pass{ColorMetadataPass::Mode::EliminateFastClear, {color}}; const auto memoryHolds = [&](std::array texel) { @@ -2350,13 +2363,15 @@ void metadataPassTests(const Device& device, Recorder& recorder) { return true; }; const auto keysUncompressed = [&] { return ReadDccKeys(color.dccAddress, surfaceBytes) == DccKeys::Uncompressed; }; + const auto extent = DccKeyCount(TextureTileMode::kR64KBX, 4, side, side, surfaceBytes); std::memset(texels, 0x55, surfaceBytes); - std::memset(keys, 0x20, keyCount); + std::memset(keys, 0x20, extent); RunColorMetadataPass(context, pass); Require(StorageTexture::FindPending(address, surfaceBytes) != nullptr && texels[0] == 0x55, "the register fast clear eliminate did not stay in the resident image"); Require(keysUncompressed(), "the register fast clear eliminate left the keys compressed"); + Require(CurrentDccKeys(color.dccAddress, surfaceBytes, extent) == DccKeys::Uncompressed, "the register fast clear eliminate did not store uncompressed keys over the target's console DCC extent"); Require(memoryHolds({0x10, 0x20, 0x40, 0x80}), "the register fast clear eliminate did not store CB_COLOR_CLEAR_WORD"); - Require(std::all_of(keys, keys + keyCount, [](std::uint8_t key) { return key == 0xff; }), "the stored keys are not uncompressed"); + Require(std::all_of(keys, keys + extent, [](std::uint8_t key) { return key == 0xff; }), "the stored keys are not uncompressed"); std::memset(keys, 0xc0, keyCount); RunColorMetadataPass(context, {ColorMetadataPass::Mode::DccDecompress, {color}}); Require(StorageTexture::FindPending(address, surfaceBytes) != nullptr, "the DCC decompress did not stay in the resident image"); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 046e79283..ab54f4035 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -327,7 +327,7 @@ void testCompressedPresentation() { std::vector allocation(6 * 65536 + 65535); const auto storage = alignedBuffer(allocation); fillBuffer(storage, 259, 137); - std::vector keys(storage.size() / 256); + std::vector keys(4096); VideoOutBuffers buffer{storage.data(), keys.data(), {nullptr, nullptr}}; VideoOutBufferAttribute2 attribute{}; sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0x208, 0xff102030); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6ee7e8f4d..3fe6e8d8b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -295,7 +295,8 @@ Throughout the project, every function at every stage either **does exactly what - [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target - [Host imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. - [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. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. PS5 equivalence has not been measured on console hardware. -- [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. +- [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. +- [DCC key stores](../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect.