fix(agc): a key fill over other images stays inside the image's DCC key extent; test the fill cover

When another live image overlaps the fill, a fill at a live image's dccAddress is a key fill
only while it stays within DccKeyCount (the pipe-aligned key count); with nothing overlapping
the length rule is unchanged. ClassifyFill, NoteKeysFill and ClearByKeysFill share the match.
agc_driver_fill_cover_keys pins Keys, Inside and Several for fills at and beside the keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michon 2026-10-08 18:06:37 +02:00
parent fc05d1e3e3
commit 7208d7d314
4 changed files with 266 additions and 11 deletions

View File

@ -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;

View File

@ -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<StorageTexture*> 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<unsigned long long>(texture->descriptor.baseAddress), static_cast<unsigned long long>(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<std::shared_ptr<StorageTexture>> 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));
}
}

View File

@ -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 <windows.h>
#endif
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <vector>
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<std::uint32_t, 8> WriteCode{
0x7e020280, 0x7e0402f2, 0x7e0602f2, 0x7e0802f2, 0x7e0a02f2, 0xf0201f08, 0x00010200, 0xbf810000,
};
std::uint64_t AddressOf(const void* data) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(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<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
#else
block = static_cast<std::uint8_t*>(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<unsigned long long>(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<std::uint32_t, 8> 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<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((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<std::uint32_t>(meta & 0xffu) << 24u),
static_cast<std::uint32_t>(meta >> 8u),
};
}
void Dispatch(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData) {
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(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<StorageTexture> Create(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint64_t keys) {
std::vector<std::uint32_t> 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<std::shared_ptr<StorageTexture>> 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;
}
}

View File

@ -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.