From 626cff7d26f31ca3d0f7ffdff7c0bfe6c740e920 Mon Sep 17 00:00:00 2001 From: SP4C3B4R-8 <209078228+SP4C3B4R-8@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:49:48 +0200 Subject: [PATCH] fix(libSceAgcDriver): retire a depth surface once another writer covers its memory A depth surface lives only in its Vulkan image, so a view of its memory with another layout could only throw, and a matching view kept reading the image after other writes over its memory. Retire the surface when a color target or a written storage image it does not serve overlaps its planes, or when a view it cannot serve finds guest memory over the planes written since the surface was last bound; lookups then skip it and the view reads the memory like any texture. Binding the target again brings the surface back. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 87 ++++++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../Graphics/src/ShaderResources.cpp | 14 +- .../tests/execution/DepthStorageReuse.cpp | 1 + .../execution/DepthSurfaceLastWriter.cpp | 245 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 7 files changed, 331 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index a01eefaefd..1a928704b7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -22,6 +22,7 @@ std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32 void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes); void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const DepthTarget& target, std::span> sampled); void ClearDepthSurfaces(VkDevice device); +void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes); bool DepthSurfaceAt(std::uint64_t address); bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0bfdcac635..dede83aaa9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -235,6 +235,15 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } + bool SampledAccepts(std::span words, const GuestTextureResource& resource) const { + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); + const bool oneSlice = target.samples != 1 ? resource.dimension == TextureDimension::k2DMsaa : resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); + return (ResolveTextureFormat(resource.format) == expected || depthBits) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0; + } + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { CheckCurrent(); CheckResource(resource); @@ -248,17 +257,16 @@ public: const bool multisampled = target.samples != 1; Require(!multisampled || sampleable, "the device cannot sample the native depth target's format and sample count"); const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const auto format = ResolveTextureFormat(resource.format); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; - const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - const bool oneSlice = multisampled ? resource.dimension == TextureDimension::k2DMsaa : resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); if (multisampled) { Require(!stencil && !depthBits && format == expected, "native multisampled depth sampling needs a numeric depth-plane format"); Require(resource.samples == target.samples, "the sampled depth descriptor's sample count differs from its native target"); Require(hostFormat == target.format, "sampling a native multisampled depth plane that the device stores in another depth format is not implemented"); } - if ((format != expected && !depthBits) || !oneSlice || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + if (!SampledAccepts(words, resource)) { char text[448]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), @@ -298,6 +306,26 @@ public: } std::uint64_t written = ++depthWrites; + bool retired = false; + + bool Overlaps(std::uint64_t address, std::uint64_t bytes) const { + const auto end = address + bytes; + if (address < target.address + depthBytes() && target.address < end) return true; + return target.stencilAddress != 0 && address < target.stencilAddress + stencilBytes() && target.stencilAddress < end; + } + + void NoteMemory() { + depthMemory = GuestMemory::CollectWrites(target.address, static_cast(depthBytes())); + stencilMemory = target.stencilAddress != 0 ? GuestMemory::CollectWrites(target.stencilAddress, static_cast(stencilBytes())) : 0; + } + + bool OverwrittenInMemory() const { + GuestMemory::CollectWrites(target.address, static_cast(depthBytes())); + if (GuestMemory::WrittenSince(target.address, static_cast(depthBytes()), depthMemory)) return true; + if (target.stencilAddress == 0) return false; + GuestMemory::CollectWrites(target.stencilAddress, static_cast(stencilBytes())); + return GuestMemory::WrittenSince(target.stencilAddress, static_cast(stencilBytes()), stencilMemory); + } VkImageAspectFlags pendingClear = 0; float clearDepth = 0.0f; @@ -422,6 +450,17 @@ private: std::map, std::shared_ptr> textures; PlaneStorage depthPlane; PlaneStorage stencilPlane; + std::uint64_t depthMemory = 0; + std::uint64_t stencilMemory = 0; + + std::uint64_t depthBytes() const { + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return DepthSliceBytes(target.extent, d16 ? 2u : 4u); + } + + std::uint64_t stencilBytes() const { + return DepthSliceBytes(target.extent, 1u); + } bool CoversPlane(const StorageTexture& storage, bool stencil) const { const auto& resource = storage.Descriptor(); @@ -801,7 +840,7 @@ DepthSurface* depthSlice(VkDevice device, std::uint64_t address, const DepthTarg if (target.extent.width == base.extent.width && target.extent.height == base.extent.height && target.format == base.format) return found->second.surface; } const auto& list = surfaces(); - const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == device && !surface->retired && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); return slice == list.rend() ? nullptr : slice->get(); } @@ -814,11 +853,18 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { if (hasStencil) return stencil->second.surface; const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); + return surface->context.device == device && !surface->retired && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); }); return found == list.rend() ? nullptr : found->get(); } +void retire(DepthSurface& surface) { + surface.retired = true; + for (auto* owners : {&depthOwners(), &stencilOwners()}) { + std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); + } +} + std::map& clearedHtiles() { static auto* cleared = new std::map(); return *cleared; @@ -948,6 +994,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) } surface = surfaces().back().get(); } + surface->retired = false; const bool inherits = depthOwners().contains({context.device, target.address}); surface->clearDepth = target.clearDepth; surface->clearStencil = target.clearStencil; @@ -959,6 +1006,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) } surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; + surface->NoteMemory(); drawnDepth = surface; return surface->view; } @@ -1025,7 +1073,7 @@ void ClearDepthSurfaces(VkDevice device) { std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { const auto held = [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + return surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }; { std::lock_guard lock(surfacesMutex()); @@ -1055,14 +1103,20 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); const bool integer = d16 && format == VK_FORMAT_R16_UINT && !depthBits; if (!stencil && format != (d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT) && !depthBits && !integer) return nullptr; - found->ApplyFastClear(); const bool cube = resource.dimension == TextureDimension::kCube; const bool layered = cube || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray != 0); - if (stencil || (!layered && !integer)) return found->Sampled(words, resource, components); - Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); + const bool sampled = stencil || (!layered && !integer); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const bool planes = (format == copyFormat || depthBits) && resource.width == base.extent.width && resource.height == base.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && !(resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube); + if (!(sampled ? found->SampledAccepts(words, resource) : planes) && found->OverwrittenInMemory()) { + retire(*found); + return nullptr; + } + found->ApplyFastClear(); + if (sampled) return found->Sampled(words, resource, components); + Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; - if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || (resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { + if (!planes) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the depth planes of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), dimension %d, %u slices, levels %u-%u, base slice %u is not implemented", static_cast(base.address), base.extent.width, base.extent.height, static_cast(base.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.dimension), layers, resource.baseLevel, resource.lastLevel, resource.baseArray); throw std::runtime_error(text); @@ -1154,7 +1208,7 @@ std::vector layeredSlices(const Context& context, const LayeredDep const auto& list = surfaces(); for (std::uint32_t layer = storage.firstLayer; layer < storage.layers; ++layer) { const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel); - const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && !surface->retired && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); if (slice != list.rend()) slices.push_back({slice->get(), layer}); } return slices; @@ -1187,7 +1241,7 @@ std::shared_ptr DepthSurfaceStorage(const Context& context, std: std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + return surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); if (found == list.rend()) return nullptr; auto& surface = **found; @@ -1217,7 +1271,14 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); - return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); + return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return !surface->retired && (surface->target.address == address || surface->target.stencilAddress == address); }); +} + +void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device == device && !surface->retired && surface->Overlaps(address, bytes)) retire(*surface); + } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 754732b6c7..e31613eb3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1983,6 +1983,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes()); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; timer.phase(PhaseSetup); // Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index dd35a350a3..554709280d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -396,7 +396,7 @@ void logLookup(const LookupRecord& record) { log.push_back(record); } -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0); +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false); std::uint64_t heldBytes(const StorageTexture& image) { return std::max(image.AllocationBytes(), image.GuestBytes()); @@ -925,10 +925,11 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe } // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes, bool written) { Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; + if (written) RetireDepthSurfaces(context.device, viewed.baseAddress, guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes); static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); @@ -2849,7 +2850,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (storageIndex >= storageTextures.size()) return false; std::shared_ptr expected; if (SameAsPreviousStorageElement(binding, element) && StorageDedupeEnabled()) expected = storageTextures[storageIndex - 1]; - else expected = cachedStorageTexture(context, words, DecodeTextureResource(words), storageMips[storageIndex]); + else expected = cachedStorageTexture(context, words, DecodeTextureResource(words), storageMips[storageIndex], 0, storageWritten[storageIndex]); if (expected != storageTextures[storageIndex]) return false; ++storageIndex; } @@ -4013,14 +4014,15 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); + // Images the shader only reads have nothing to store back. + const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element]; // The same surface as the previous element: its image was just looked up and refreshed. if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back()); - else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes)); + else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes, written)); storageMips.push_back(mip); storageKeys.push_back(resource.dccAddress); storageFirstLayer.push_back(firstLayer); - // Images the shader only reads have nothing to store back. - storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]); + storageWritten.push_back(written); storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp index 91db433d86..f6b6127eb1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp @@ -68,6 +68,7 @@ int main() { Require(DepthFastClearHarness::Draw(*device, shaders, surface, {.depthWrite = true}) == DepthFastClearHarness::Covered, "depth producer did not cover the surface"); Read(*device, format == VK_FORMAT_D16_UNORM ? 7u : 22u, Width, true, 0.5f); Read(*device, 22u, format == VK_FORMAT_D16_UNORM ? Width : Width / 2u, false, 0.25f); + if (format == VK_FORMAT_D32_SFLOAT) Read(*device, 22u, Width, true, 0.25f); } std::puts("depth storage reuse GPU tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp new file mode 100644 index 0000000000..b7f809f6ee --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp @@ -0,0 +1,245 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint64_t DepthAddress = 0x7f0000000000ull; +constexpr VkExtent2D Extent{64, 64}; +constexpr std::uint32_t Format32Float = 22; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearDepthSurfaces(context.device); + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +constexpr std::size_t Block = 65536; + +void* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), bytes), "the test block is not watched"); + return block; +} + +DepthTarget Depth(std::uint64_t address = DepthAddress, VkExtent2D extent = Extent) { + return {address, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; +} + +GuestTextureResource View(std::uint32_t width, std::uint32_t height, std::uint64_t address = DepthAddress, std::uint32_t levels = 1) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = levels; + resource.lastLevel = levels - 1; + resource.dimension = TextureDimension::k2D; + resource.format = Format32Float; + return resource; +} + +constexpr VkComponentMapping Identity{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + +std::shared_ptr Sample(const Context& context, const GuestTextureResource& resource) { + const std::array words{0, 0, 0, 0, resource.width, resource.height, resource.lastLevel, 0}; + return DepthSurfaceTexture(context, words, resource, Identity); +} + +bool Refused(const Context& context, const GuestTextureResource& resource) { + try { + Sample(context, resource); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find("is not implemented") != std::string::npos; + } + return false; +} + +void Run(const Context& context) { + const auto mips = View(Extent.width, Extent.height, DepthAddress, 2); + const auto exact = View(Extent.width, Extent.height); + const auto depthBytes = DepthSliceBytes(Extent, 4); + + DepthSurfaceView(context, Depth()); + Require(DepthSurfaceAt(DepthAddress), "a bound depth surface is not found at its address"); + Require(Refused(context, mips), "depth, then a view with two levels: the live depth surface did not refuse it"); + Require(Sample(context, exact) != nullptr, "depth, then its own view: the depth surface did not serve it"); + + RetireDepthSurfaces(context.device, DepthAddress + depthBytes, 0x10000); + Require(Refused(context, mips), "a write past the depth plane retired the depth surface"); + + RetireDepthSurfaces(context.device, DepthAddress + 0x100, 0x1000); + Require(!DepthSurfaceAt(DepthAddress), "a color target over the depth plane left the depth surface live"); + Require(Sample(context, mips) == nullptr, "depth, then a color target, then a view with two levels: the view was not left to guest memory"); + Require(Sample(context, exact) == nullptr, "depth, then a color target, then its own view: the retired depth surface served it"); + + DepthSurfaceView(context, Depth()); + Require(DepthSurfaceAt(DepthAddress), "binding the depth target again did not bring the depth surface back"); + Require(Refused(context, mips), "depth, color target, depth again, then a view with two levels: the live depth surface did not refuse it"); + + if (!AgcDriver::GuestMemory::WriteWatched()) { + Require(Refused(context, mips), "without write watching, a view with two levels was not refused"); + std::puts("guest memory is not write-watched here: the CPU write order is not exercised"); + return; + } + + auto* memory = static_cast(AllocateWatched(2 * Block)); + const auto watched = reinterpret_cast(memory); + Require(depthBytes <= 2 * Block, "the depth plane does not fit the watched block"); + const auto watchedMips = View(Extent.width, Extent.height, watched, 2); + const auto watchedExact = View(Extent.width, Extent.height, watched); + AgcDriver::GuestMemory::CollectWrites(watched, 2 * Block); + + DepthSurfaceView(context, Depth(watched)); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Refused(context, watchedMips), "depth, then a view with two levels: the live depth surface over watched memory did not refuse it"); + + memory[8] = 0x5a; + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Sample(context, watchedExact) != nullptr, "depth, then a CPU write, then its own view: the depth surface did not serve it"); + Require(Sample(context, watchedMips) == nullptr, "depth, then a CPU write, then a view with two levels: the view was not left to guest memory"); + Require(!DepthSurfaceAt(watched), "depth, then a CPU write, then a view with two levels: the depth surface stayed live"); + + memory[16] = 0xa5; + AgcDriver::GuestMemory::BumpCollectEpoch(); + DepthSurfaceView(context, Depth(watched)); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Refused(context, watchedMips), "a CPU write, then depth, then a view with two levels: the live depth surface did not refuse it"); + + AgcDriver::GuestMemory::MarkWritten(watched + Block, 4); + Require(Refused(context, watchedMips), "a driver store past the depth plane retired the depth surface"); + AgcDriver::GuestMemory::MarkWritten(watched + 0x100, 4); + Require(Sample(context, watchedMips) == nullptr, "depth, then a driver store, then a view with two levels: the view was not left to guest memory"); +} + +} + +int main() { + try { + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + Run(device->GetContext()); + std::puts("depth surface last writer tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 9548068618..da430797b8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -39,6 +39,7 @@ - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. +- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured.