fix(agc): remake a surface's storage image when its DCC keys move

The storage image cache keys a surface by memory, extent, layout and
storage format, and the image keeps the DCC key range of the first
descriptor that made it. When a later descriptor names other metadata
(the title reallocated the keys, or the memory held another surface
before), the image kept the old keys: fast clears of the new keys never
reached it, and its write-backs marked keys the surface no longer owns.

A descriptor whose DCC address differs from the cached image's now evicts
that image (its pending results are flushed first) and a new one is made
from memory under the named keys. A sampled view of another image's
pending results is taken only under the same keys. A descriptor without
metadata keeps whatever image the surface has.
This commit is contained in:
Dean Galvin 2026-10-01 00:25:17 +13:00
parent fc0596240d
commit f25695fac7
2 changed files with 112 additions and 2 deletions

View File

@ -196,6 +196,10 @@ void logLookup(const LookupRecord& record) {
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0);
bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) {
return resource.dccAddress != 0 && image.Descriptor().dccAddress != resource.dccAddress;
}
// Whether a sampled texture over `resource` can be a view of the surface's cached storage image
// instead of a CPU snapshot (see cachedTexture): the format has a storage form and is not block
// compressed, and the surface lives in host-imported memory, where the image uploads and refreshes
@ -287,7 +291,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// the GPU supplies the texture by a view of it; anything else needs those results in guest
// memory first.
auto source = StorageTexture::FindPending(address, guestBytes);
if (source != nullptr && !Texture::CanCopyFrom(*source, resource)) source.reset();
if (source != nullptr && (!Texture::CanCopyFrom(*source, resource) || MetadataMoved(*source, resource))) source.reset();
// Otherwise a surface in host-imported memory is viewed through its cached storage image (made
// here when there is none): its refresh after a CPU or GPU write is a GPU-direct detile from the
// import, recorded behind the producer, so no bytes are read or compared on the CPU and nothing
@ -496,7 +500,12 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
const StorageKey key{context.device, SurfaceKey(context, resource)};
auto& cache = StorageTextures();
std::lock_guard lock(cache.mutex);
if (auto it = findStorage(cache, key); it != cache.entries.end()) {
auto it = findStorage(cache, key);
if (it != cache.entries.end() && MetadataMoved(*it->texture, resource)) {
evictStorage(cache, it);
it = cache.entries.end();
}
if (it != cache.entries.end()) {
it->texture->Refresh();
cache.entries.splice(cache.entries.begin(), cache.entries, it);
counters.storageHits.fetch_add(1, std::memory_order_relaxed);

View File

@ -4,6 +4,8 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@ -106,6 +108,7 @@ public:
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
context.limits = properties.limits;
context.bufferDeviceAddress = true;
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool.queueFamilyIndex = family;
@ -596,6 +599,103 @@ void storeRunTests(const Device& device, Recorder& recorder) {
HostImportFor(context, address, bytes);
}
void movedMetadataTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
std::cout << "host imports unavailable: moved DCC metadata not tested\n";
return;
}
constexpr std::uint32_t side = 256;
constexpr std::size_t surfaceBytes = side * side * 4;
constexpr std::size_t keyCount = surfaceBytes / 256;
constexpr std::size_t bytes = surfaceBytes + 65536;
#ifdef _WIN32
void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
void* block = std::aligned_alloc(65536, bytes);
#endif
Require(block != nullptr, "cannot allocate the moved metadata block");
auto* texels = static_cast<std::uint8_t*>(block);
auto* firstKeys = texels + surfaceBytes;
auto* secondKeys = firstKeys + 4096;
const auto address = reinterpret_cast<std::uint64_t>(block);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true);
}
struct Unregister {
const Context& context;
void* block;
std::uint64_t address;
~Unregister() {
ClearCachedTextures(context.device);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, address, bytes);
}
} unregister{base, block, address};
if (HostImportFor(base, address, bytes) == nullptr) {
std::cout << "host import of the moved metadata block refused: moved DCC metadata not tested\n";
return;
}
TextureDetiler detiler(base);
auto context = base;
context.detiler = &detiler;
GuestTextureResource resource{};
resource.baseAddress = address;
resource.width = side;
resource.height = side;
resource.mipCount = 1;
resource.tileMode = TextureTileMode::kR64KBX;
resource.dimension = TextureDimension::k2D;
resource.format = 56;
resource.dstSelX = 4;
resource.dstSelY = 5;
resource.dstSelZ = 6;
resource.dstSelW = 7;
Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the moved metadata surface has an unexpected size");
const auto first = address + surfaceBytes;
const auto second = first + 4096;
const auto withKeys = [&](std::uint64_t keys) {
auto described = resource;
described.dccAddress = keys;
return described;
};
const auto holds = [&](const StorageTexture& image, std::array<std::uint8_t, 4> texel) {
Buffer readback(context, surfaceBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
const auto commands = recorder.Commands();
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {side, side, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, image.Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Submit();
device.WaitQueue();
recorder.Sync();
const auto pixels = readback.Bytes();
for (std::size_t i = 0; i < pixels.size(); ++i) {
if (std::to_integer<std::uint8_t>(pixels[i]) != texel[i % 4]) return false;
}
return true;
};
std::memset(texels, 0x55, surfaceBytes);
std::memset(firstKeys, 0xff, keyCount);
std::memset(secondKeys, 0x00, keyCount);
const auto original = CachedStorageSurface(context, withKeys(first));
Require(original->Descriptor().dccAddress == first && holds(*original, {0x55, 0x55, 0x55, 0x55}), "the first image does not hold the stored texels");
const auto moved = CachedStorageSurface(context, withKeys(second));
Require(moved->Descriptor().dccAddress == second, "the storage image kept the keys the surface no longer names");
Require(!StorageImageCached(context, original.get()), "the image of the old keys is still the surface's");
Require(holds(*moved, {0, 0, 0, 0}), "a fast clear of the moved keys did not reach the image");
Require(CachedStorageSurface(context, resource) == moved && moved->Descriptor().dccAddress == second, "a descriptor without metadata replaced the image");
const auto back = CachedStorageSurface(context, withKeys(first));
Require(back != moved && back->Descriptor().dccAddress == first, "the keys moving back did not remake the image");
Require(holds(*back, {0x55, 0x55, 0x55, 0x55}), "the image remade under the first keys does not hold the stored texels");
}
void resourceReadTests(const Device& device, Recorder& recorder) {
const auto& context = device.GetContext();
if (context.hostImportAlignment == 0) {
@ -971,6 +1071,7 @@ int main() {
keyProofTests(device, recorder);
resourceReadTests(device, recorder);
storeRunTests(device, recorder);
movedMetadataTests(device, recorder);
unitShadowTests(device, recorder);
dataWordPositionsTests();
dataRefreshTests(device, recorder);