diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 0ce33ebbe..20b778303 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -226,8 +226,9 @@ public: // `image` and `layer` naming it, whatever other images lie over the range (`others`, of which // `inside` wholly inside it: stale images of earlier uses of the memory, which the title's // transient allocator hands out again); otherwise part of one image (the fill inside the - // surface, the surface inside the fill, or straddling), several, or none but exactly one - // surface's DCC metadata. Images the cache let go (see Flush) do not count. + // surface, the surface inside the fill, or straddling), several, or a fill of one surface's DCC + // keys (Keys: at its dccAddress and at least one key long, and while other images overlap it no + // longer than its key extent; see keysFillMatches). Images the cache let go (see Flush) do not count. enum class FillCover { None, Exact, Inside, Around, Straddle, Several, Keys, Layer }; struct FillCoverage { FillCover cover = FillCover::None; @@ -412,6 +413,12 @@ private: void forgetBorrowed(std::uint32_t first, std::uint32_t count); bool clearByKeysFill(DccKeys keys, std::uint8_t key); bool overlaps(std::uint64_t address, std::size_t bytes) const; + // Whether the image is live (not released) and overlaps the fill: the test that gives ClassifyFill + // its overlapping images, and that NoteKeysFill and ClearByKeysFill use to bound the key match. + bool overlapsLive(std::uint64_t address, std::size_t bytes) const; + // Whether a fill of [address, address + bytes) is a fill of this image's DCC keys, `overlapped` + // saying whether any live image overlaps the fill (see Texture.cpp). + bool keysFillMatches(std::uint64_t address, std::size_t bytes, bool overlapped) const; bool pendingUnitInside(std::uint64_t address, std::size_t bytes) const; VkImageView createView(std::uint32_t mip, bool firstLayer, VkFormat format) const; void release() noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index b76bbc928..522a30087 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2212,6 +2212,22 @@ bool StorageTexture::overlaps(std::uint64_t address, std::size_t bytes) const { return address < descriptor.baseAddress + guestBytes && descriptor.baseAddress < address + bytes; } +bool StorageTexture::overlapsLive(std::uint64_t address, std::size_t bytes) const { + return !released && guestBytes != 0 && overlaps(address, bytes); +} + +// A fill of this image's DCC keys starts at the dccAddress and is at least one key per 256 guest +// bytes. While any live image overlaps the fill (`overlapped`), it must also stay inside the key extent +// (DccKeyCount, the pipe-aligned count, at least guestBytes / 256): the bytes past the keys may be +// memory the overlapping image owns, so a longer fill is not a key fill. The bound is inferred, not +// measured (docs/dev/TechnicalDebt.md). With nothing overlapping, the length rule alone decides, as +// it did before the bound. +bool StorageTexture::keysFillMatches(std::uint64_t address, std::size_t bytes, bool overlapped) const { + constexpr std::uint64_t keyBytes = 256; + if (released || descriptor.dccAddress != address || guestBytes / keyBytes == 0 || bytes < guestBytes / keyBytes) return false; + return !overlapped || bytes <= DccKeyCount(descriptor, guestBytes); +} + void StorageTexture::MarkDirty() { if (descriptor.dccAddress != 0 && IsDccClear(uploadedKeys) && !IsDccClear(filledKeys)) { traceKeyStore("first write", descriptor, guestBytes); @@ -2689,7 +2705,7 @@ StorageTexture::FillCoverage StorageTexture::ClassifyFill(std::uint64_t address, std::lock_guard lock(live.mutex); std::vector overlapping; for (auto* texture : live.textures) { - if (!texture->released && texture->guestBytes != 0 && texture->overlaps(address, bytes)) overlapping.push_back(texture); + if (texture->overlapsLive(address, bytes)) overlapping.push_back(texture); } const auto end = address + bytes; StorageTexture* covered = nullptr; @@ -2730,10 +2746,7 @@ StorageTexture::FillCoverage StorageTexture::ClassifyFill(std::uint64_t address, if (texture->descriptor.baseAddress >= address && texture->descriptor.baseAddress + texture->guestBytes <= end) ++coverage.inside; } } - } else if (std::any_of(live.textures.begin(), live.textures.end(), [&](const StorageTexture* texture) { - constexpr std::uint64_t keyBytes = 256; - return !texture->released && texture->descriptor.dccAddress == address && texture->guestBytes / keyBytes != 0 && bytes >= texture->guestBytes / keyBytes; - })) { + } else if (std::any_of(live.textures.begin(), live.textures.end(), [&](const StorageTexture* texture) { return texture->keysFillMatches(address, bytes, !overlapping.empty()); })) { coverage.cover = FillCover::Keys; } else if (overlapping.size() > 1) { coverage.cover = FillCover::Several; @@ -2775,13 +2788,13 @@ std::size_t StorageTexture::NoteKeysFill(std::uint64_t address, std::size_t byte case 0xff: keys = DccKeys::Uncompressed; break; default: return 0; } - constexpr std::uint64_t keyBytes = 256; static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; auto& live = Live(); std::lock_guard lock(live.mutex); + const bool overlapped = std::any_of(live.textures.begin(), live.textures.end(), [&](const StorageTexture* texture) { return texture->overlapsLive(address, bytes); }); std::size_t covered = 0; for (auto* texture : live.textures) { - if (texture->released || texture->descriptor.dccAddress != address || texture->guestBytes / keyBytes == 0 || bytes < texture->guestBytes / keyBytes) continue; + if (!texture->keysFillMatches(address, bytes, overlapped)) continue; texture->filledKeys = keys; ++covered; if (traceKeys && IsDccClear(keys)) std::fprintf(stderr, "[dcc-keys] %s key fill over 0x%llx+0x%llx (uploaded %s, dirty %d)\n", DccKeysName(keys), static_cast(texture->descriptor.baseAddress), static_cast(texture->guestBytes), DccKeysName(texture->uploadedKeys), texture->dirty ? 1 : 0); @@ -2803,13 +2816,13 @@ std::size_t StorageTexture::ClearByKeysFill(std::uint64_t address, std::size_t b case 0x20: keys = DccKeys::ClearRegister; break; default: return 0; } - constexpr std::uint64_t keyBytes = 256; std::vector> covered; { auto& live = Live(); std::lock_guard lock(live.mutex); + const bool overlapped = std::any_of(live.textures.begin(), live.textures.end(), [&](const StorageTexture* texture) { return texture->overlapsLive(address, bytes); }); for (auto* texture : live.textures) { - if (texture->released || texture->descriptor.dccAddress != address || texture->guestBytes / keyBytes == 0 || bytes < texture->guestBytes / keyBytes) continue; + if (!texture->keysFillMatches(address, bytes, overlapped)) continue; if (auto shared = texture->weak_from_this().lock()) covered.push_back(std::move(shared)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FillCoverKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FillCoverKeys.cpp new file mode 100644 index 000000000..7d4084a27 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FillCoverKeys.cpp @@ -0,0 +1,234 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::DccKeyCount; +using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using Cover = StorageTexture::FillCover; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t TileRenderTarget64KB = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t KeyBytes = SurfaceBytes / 256; +// The pipe-aligned key extent of the 128x128 surface (one 4 KiB metadata block): longer than the +// one-key-per-256-bytes count, so a fill between the two is told apart from a key-sized one. +constexpr std::size_t KeyExtent = 4096; +// Each scenario's images sit in their own region, so no fill of one overlaps the other's images. +constexpr std::size_t Region = 0x40000; +constexpr std::size_t BlockBytes = 2 * Region; + +alignas(256) constexpr std::array WriteCode{ + 0x7e020280, 0x7e0402f2, 0x7e0602f2, 0x7e0802f2, 0x7e0a02f2, 0xf0201f08, 0x00010200, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +// Guest memory for the surfaces, registered as allocated so the driver reads and writes it. +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "fill cover test: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +const char* CoverName(Cover cover) { + switch (cover) { + case Cover::None: return "none"; + case Cover::Exact: return "exact"; + case Cover::Inside: return "inside"; + case Cover::Around: return "around"; + case Cover::Straddle: return "straddle"; + case Cover::Several: return "several"; + case Cover::Keys: return "keys"; + case Cover::Layer: return "layer"; + } + return "?"; +} + +void ExpectCover(std::uint64_t address, std::size_t bytes, Cover expected, const char* what) { + const auto coverage = StorageTexture::ClassifyFill(address, bytes); + if (coverage.cover == expected) return; + char text[256]; + std::snprintf(text, sizeof(text), "%s: fill 0x%llx+0x%zx is %s, expected %s", what, static_cast(address), bytes, CoverName(coverage.cover), CoverName(expected)); + Expect(false, text); +} + +// The 128x128 RGBA8 surface at `address` with its DCC keys at `keys` (none when 0), with the words +// DccKeysFirstWrite.cpp lays out; the keys are pipe-aligned so their extent is KeyExtent. +std::array SurfaceWords(std::uint64_t address, std::uint64_t keys) { + const auto meta = keys >> 8u; + const std::uint32_t metaBits = keys != 0 ? (1u << 19u) | (1u << 20u) | (1u << 21u) : 0u; + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileRenderTarget64KB << 20u) | (Type2D << 28u), + 0u, + 0u, + metaBits | (static_cast(meta & 0xffu) << 24u), + static_cast(meta >> 8u), + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +// The storage image the title's first write creates for the surface at `address`: a dispatch that +// stores into it, flushed at once so that no later write-back of it covers memory another image is +// checked over. The GPU mutex is taken here, not held by the caller. +std::shared_ptr Create(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint64_t keys) { + std::vector userData(16, 0u); + const auto words = SurfaceWords(address, keys); + std::copy(words.begin(), words.end(), userData.begin() + 4); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, userData); + StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "fill cover test"); + device.WaitIdle(); + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + Require(image != nullptr && image->Descriptor().dccAddress == keys, "fill cover test: the dispatch did not create the image under its keys"); + return image; +} + +// Scenario one: image B (no DCC) and the image C beside it cover [0, 0x20000) of the region; image A +// (texels at 0x20000) has its keys at 0x8000, inside B, so a fill there meets B and only B. +// Scenario two: image D (texels at the region's start) has its keys at 0x10000, outside every other +// image, so a fill there meets nothing. +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + const auto base = AddressOf(block); + const auto aKeys = base + 0x8000; + const auto dKeys = base + Region + 0x10000; + std::vector> images; + + std::memset(block, 0x55, 0x30000); + images.push_back(Create(device, base, 0)); + images.push_back(Create(device, base + 0x10000, 0)); + // The keys go in after B's write-back, which would store B's texels over them. + std::memset(block + 0x8000, 0xff, KeyExtent); + images.push_back(Create(device, base + 0x20000, aKeys)); + const auto& keyed = *images.back(); + const auto extent = DccKeyCount(keyed.Descriptor(), keyed.GuestBytes()); + Require(extent == KeyExtent, "fill cover test: the keyed image's key extent is not the pipe-aligned one"); + + std::memset(block + Region, 0x55, SurfaceBytes); + std::memset(block + Region + 0x10000, 0xff, KeyExtent); + images.push_back(Create(device, base + Region, dKeys)); + const auto alone = images.back(); + + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + // (a) a key-sized fill (and one up to the whole key extent) at the keys of an image whose keys + // lie in another live image: the key fill the bloom target's keys get over dead aliased images. + ExpectCover(aKeys, KeyBytes, Cover::Keys, "a key-sized fill over the keys of an image inside another live image"); + ExpectCover(aKeys, extent, Cover::Keys, "a fill of the whole key extent over the keys of an image inside another live image"); + // (c) past the key extent, the same fill also writes the memory after the keys, which the + // overlapping image may own: not a key fill (inferred, see docs/dev/TechnicalDebt.md). + ExpectCover(aKeys, extent + 1, Cover::Inside, "a fill one byte past the key extent, over another live image"); + // (b) the same lengths not at a key address stay what they were. + ExpectCover(aKeys + 0x100, KeyBytes, Cover::Inside, "a key-sized fill not at a dccAddress, over another live image"); + ExpectCover(aKeys + 0x100, 0x10000, Cover::Several, "a fill not at a dccAddress over two live images"); + // The consumer applies the same match: a fill past the key extent notes no keys, a key-sized one + // notes them on its image. + Expect(StorageTexture::NoteKeysFill(aKeys, extent + 1, 0x00) == 0 && keyed.FilledKeys() == DccKeys::Uncompressed, "NoteKeysFill noted keys for a fill past the key extent over another live image"); + Expect(StorageTexture::NoteKeysFill(aKeys, KeyBytes, 0x00) == 1 && keyed.FilledKeys() == DccKeys::Clear0000, "NoteKeysFill did not note a key-sized fill over an image inside another live image"); + // (d) with nothing overlapping, the length rule alone decides, as before the bound. + ExpectCover(dKeys, KeyBytes, Cover::Keys, "a key-sized fill at the keys of an image nothing else overlaps"); + ExpectCover(dKeys, 2 * extent, Cover::Keys, "a fill past the key extent at the keys of an image nothing else overlaps"); + Expect(StorageTexture::NoteKeysFill(dKeys, 2 * extent, 0x00) == 1 && alone->FilledKeys() == DccKeys::Clear0000, "NoteKeysFill refused a fill past the key extent with nothing overlapping"); +} + +} + +int main() { + try { + // The block outlives the device: the device's images are created over it. + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, block.Data()); + if (failures != 0) { + std::fprintf(stderr, "%d fill cover checks failed\n", failures); + return 1; + } + std::puts("fill cover keys tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f4812dd22..a276cbd1b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -303,6 +303,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 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. +- [DCC key fills over other images](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). - [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.