From 1f11b8ab9be0eca9c008df566bf11efce99cd578 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Sun, 18 Oct 2026 21:37:23 -0400 Subject: [PATCH 1/3] fix(agc): bound the sampled depth-plane copies A depth plane copy (a depth surface sampled as several slices, an integer view or a cube) was kept until the device went away, with a staging buffer of its own size, and a staging buffer it outgrew was never freed. Every depth surface a scene ever sampled this way held its copy, so device memory grew with each new scene (0.4-1.2 GB in RE2 after a level load). The copies now share their image's ownership with the views they hand out, the recorder keeps the image and the staging buffer until the recorded copy has run, and past APS5_DEPTH_PLANE_COPY_MIB (default 256; 0 keeps every copy) the least recently used copies leave the cache. A copy made again refills from the surface and guest memory as a new one does. agc_driver_depth_plane_sampling_evicting runs the plane sampling cases with a 1 MiB budget. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../Graphics/src/DepthSurface.cpp | 84 ++++++++++++++----- docs/dev/ENVIRONMENT.md | 1 + 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2c79ef109ed..463796c7031 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2141,6 +2141,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_depth_plane_sampling_evicting COMMAND agc_driver_depth_plane_sampling_tests) + set_tests_properties(agc_driver_depth_plane_sampling_evicting PROPERTIES ENVIRONMENT "APS5_DEPTH_PLANE_COPY_MIB=1" SKIP_RETURN_CODE 77 TIMEOUT 120) + if(WIN32) + set_tests_properties(agc_driver_depth_plane_sampling_evicting PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 458af384d52..17800bb4b03 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1218,8 +1218,10 @@ class DepthPlaneCopy { allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); - stagingSize = static_cast(sliceBytes()) * layers; - staging = std::make_unique(context, stagingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + imageBytes = requirements.size; + // The views share the image's ownership: the copy can leave the cache while a draw or a + // cached resource set still samples it. + owned = std::make_shared(context, image, memory); } catch (...) { release(); throw; @@ -1238,12 +1240,7 @@ class DepthPlaneCopy { std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast(sliceBytes()), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.tileMode, geometry.layers, static_cast(geometry.sliceLinearBytes), geometry.mips.size()); throw std::runtime_error(text); } - const auto stagingBytes = static_cast(geometry.sliceLinearBytes) * layers; - if (stagingBytes > stagingSize) { - retiredStaging.push_back(std::move(staging)); - staging = std::make_unique(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - stagingSize = stagingBytes; - } + const auto stagingBytes = std::max(static_cast(geometry.sliceLinearBytes), static_cast(sliceBytes())) * layers; std::vector> uploads; std::vector uploadLayers; std::vector copiedLayers; @@ -1269,11 +1266,16 @@ class DepthPlaneCopy { uploadLayers.push_back(layer); held = current; } - if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); + if (!copiedLayers.empty() || !uploads.empty()) { + // The recorder keeps the staging buffer (and the image) until the copy ran, so a copy + // evicted meanwhile, or a staging buffer outgrown, does not free memory in use. + if (staging == nullptr || stagingBytes > staging->Size()) staging = std::make_shared(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + record(slices, resource, geometry, copiedLayers, uploads, uploadLayers, staging); + } const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer}; auto& texture = textures[key]; if (texture == nullptr) { - texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer); + texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer, owned); texture->MarkRefreshedPerUse(); } return texture; @@ -1283,6 +1285,9 @@ class DepthPlaneCopy { const VkExtent2D extent; const VkFormat format; const std::uint32_t layers; + VkDeviceSize imageBytes = 0; + std::uint64_t lastUse = 0; + VkDeviceSize HeldBytes() const { return imageBytes + (staging != nullptr ? staging->Size() : 0u); } private: struct GuestLayer { @@ -1295,15 +1300,14 @@ class DepthPlaneCopy { VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; - std::unique_ptr staging; - std::size_t stagingSize = 0; - std::vector> retiredStaging; + std::shared_ptr owned; + std::shared_ptr staging; std::map, std::shared_ptr> textures; std::vector guestLayers; std::vector surfaceLayers; bool filled = false; - void record(std::span slices, const GuestTextureResource& resource, const SurfaceGeometry& geometry, std::span copiedLayers, std::span> uploads, std::span uploadLayers) { + void record(std::span slices, const GuestTextureResource& resource, const SurfaceGeometry& geometry, std::span copiedLayers, std::span> uploads, std::span uploadLayers, const std::shared_ptr& staging) { if (!uploads.empty()) { Require(context.detiler != nullptr, "depth plane copy requires a texture detiler"); context.detiler->BeginBatch(); @@ -1360,7 +1364,11 @@ class DepthPlaneCopy { barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); filled = true; if (batch) batch->SubmitAndWait(); - else Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + else { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + recorder->Keep(staging, staging->Size()); + if (owned != nullptr) recorder->Keep(owned); + } } VkDeviceSize sliceBytes() const { @@ -1374,8 +1382,11 @@ class DepthPlaneCopy { void release() noexcept { textures.clear(); staging.reset(); - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + if (owned != nullptr) owned.reset(); + else { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } image = VK_NULL_HANDLE; memory = VK_NULL_HANDLE; } @@ -1386,6 +1397,37 @@ std::map& keep) { + static const VkDeviceSize budget = [] { + const char* text = std::getenv("APS5_DEPTH_PLANE_COPY_MIB"); + return static_cast(text != nullptr ? std::strtoull(text, nullptr, 10) : 256ull) << 20u; + }(); + if (budget == 0) return; + auto& copies = planeCopies(); + VkDeviceSize total = 0; + for (const auto& entry : copies) total += entry.second->HeldBytes(); + while (total > budget) { + auto oldest = copies.end(); + for (auto it = copies.begin(); it != copies.end(); ++it) { + if (it->first == keep) continue; + if (oldest == copies.end() || it->second->lastUse < oldest->second->lastUse) oldest = it; + } + if (oldest == copies.end()) break; + total -= oldest->second->HeldBytes(); + copies.erase(oldest); + } +} + struct LayeredSlice { DepthSurface* surface; std::uint32_t layer; @@ -2372,10 +2414,14 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanimage, slice == drawnDepth ? 0u : slice->written}); } const auto imageFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT; - auto& copy = planeCopies()[{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}]; - if (copy == nullptr) { + auto& copies = planeCopies(); + const std::tuple copyKey{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}; + if (auto& copy = copies[copyKey]; copy == nullptr) { copy = std::make_unique(context, base.extent, imageFormat, layers); + evictPlaneCopies(copyKey); } + auto& copy = copies[copyKey]; + copy->lastUse = ++planeCopyClock(); return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); } diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index ca8bf9bc152..2435a84081e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -109,6 +109,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_CPU_STORES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | writes packet stores on the CPU instead of recording them on the GPU | | `APS5_DCC_KEYS_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every DCC key store at once instead of batching them | | `APS5_DEPTH_COPY_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | minimum surface size in KiB for a depth target to take the depth-copy path | +| `APS5_DEPTH_PLANE_COPY_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | 256 | budget in MiB of the sampled depth-plane copies kept for reuse (least recently used copies leave first); `0` keeps every copy | | `APS5_DEPTH_PLANE_WHOLE_STORE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | stores a stale depth plane whole when memory over it is read, instead of only the 64 KiB units the read overlaps | | `APS5_DISPATCH_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 268435456 | size of the dispatch cache in bytes (0 keeps the default of 256 MiB) | | `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | From 3245a347e97777b2963372ea4602013207b0e1b8 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Mon, 19 Oct 2026 09:56:34 -0400 Subject: [PATCH 2/3] test(agc): check that the evicting depth-plane run keeps only the newest copy agc_driver_depth_plane_sampling_evicting passes --evicting, and the test then requires a single depth plane copy left in the cache (DepthPlaneCopyCount) after the cases. With APS5_DEPTH_PLANE_COPY_MIB=0, which keeps every copy, it fails with "8 depth plane copies were kept with a 1 MiB budget, expected only the newest". The explanatory comments move to the pull request. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../Graphics/include/DepthSurface.hpp | 1 + .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 13 ++++--------- .../tests/execution/DepthPlaneSampling.cpp | 7 ++++++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 463796c7031..880f08f9ad1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2141,7 +2141,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test(NAME agc_driver_depth_plane_sampling_evicting COMMAND agc_driver_depth_plane_sampling_tests) + add_test(NAME agc_driver_depth_plane_sampling_evicting COMMAND agc_driver_depth_plane_sampling_tests --evicting) set_tests_properties(agc_driver_depth_plane_sampling_evicting PROPERTIES ENVIRONMENT "APS5_DEPTH_PLANE_COPY_MIB=1" SKIP_RETURN_CODE 77 TIMEOUT 120) if(WIN32) set_tests_properties(agc_driver_depth_plane_sampling_evicting PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 835b80f59b1..96030245b3f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -72,6 +72,7 @@ VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile); std::uint64_t HtileMetadataBytes(VkExtent2D extent); bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes); std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); +std::size_t DepthPlaneCopyCount(); VkFormat HostDepthFormat(const Context& context, VkFormat format); float HostDepthBiasConstant(const Context& context, VkFormat format, float constant); std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 17800bb4b03..be31cbccf5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1219,8 +1219,6 @@ class DepthPlaneCopy { memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); imageBytes = requirements.size; - // The views share the image's ownership: the copy can leave the cache while a draw or a - // cached resource set still samples it. owned = std::make_shared(context, image, memory); } catch (...) { release(); @@ -1267,8 +1265,6 @@ class DepthPlaneCopy { held = current; } if (!copiedLayers.empty() || !uploads.empty()) { - // The recorder keeps the staging buffer (and the image) until the copy ran, so a copy - // evicted meanwhile, or a staging buffer outgrown, does not free memory in use. if (staging == nullptr || stagingBytes > staging->Size()) staging = std::make_shared(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); record(slices, resource, geometry, copiedLayers, uploads, uploadLayers, staging); } @@ -1402,11 +1398,6 @@ std::uint64_t& planeCopyClock() { return clock; } -// The plane copies were kept until the device went away: every depth surface a scene ever sampled -// held a full-size copy in device memory (0.4-1.2 GB in RE2 after a level load). Past -// APS5_DEPTH_PLANE_COPY_MIB (default 256; 0 keeps all) the least recently used copies leave the -// cache; their views keep the image alive while a draw, a recorded batch or a cached resource set -// still holds them, and a copy made again refills from the surface. void evictPlaneCopies(const std::tuple& keep) { static const VkDeviceSize budget = [] { const char* text = std::getenv("APS5_DEPTH_PLANE_COPY_MIB"); @@ -2353,6 +2344,10 @@ void ClearDepthSurfaces(VkDevice device) { UpdateResidentImageUnmapObserver_nid_no_patch(); } +std::size_t DepthPlaneCopyCount() { + return planeCopies().size(); +} + std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType) { const auto held = [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 33c6cfdb6b5..2b497f99b95 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -434,8 +434,9 @@ void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block) { } } -int main() { +int main(int argc, char** argv) { try { + const bool evicting = argc > 1 && std::string(argv[1]) == "--evicting"; GuestBlock block; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -452,6 +453,10 @@ int main() { PaddedD16CubeCase(*device, block, output); BaseSliceCase(*device, block, output); RefusalCase(*device, block); + if (evicting) { + const auto kept = AgcDriver::Graphics::DepthPlaneCopyCount(); + Require(kept == 1, std::to_string(kept) + " depth plane copies were kept with a 1 MiB budget, expected only the newest"); + } std::puts("depth plane sampling tests passed"); return 0; } catch (const std::exception& error) { From c2b128b56d7faaffce52678adbb0ee59443dc374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 04:24:25 +0200 Subject: [PATCH 3/3] fix(agc): count the staging buffer before evicting depth plane copies The newest copy's staging buffer exists only after Refresh, so evicting before it under-counted the held bytes. The evicting test now asserts a count that holds on every driver: at least the newest copy and fewer than the 8 an unbounded cache keeps. --- .../prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp | 9 ++++----- .../tests/execution/DepthPlaneSampling.cpp | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index be31cbccf5a..5836a6deeee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -2411,13 +2411,12 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span copyKey{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}; - if (auto& copy = copies[copyKey]; copy == nullptr) { - copy = std::make_unique(context, base.extent, imageFormat, layers); - evictPlaneCopies(copyKey); - } auto& copy = copies[copyKey]; + if (copy == nullptr) copy = std::make_unique(context, base.extent, imageFormat, layers); copy->lastUse = ++planeCopyClock(); - return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); + auto texture = copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); + evictPlaneCopies(copyKey); + return texture; } bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 2b497f99b95..f77432e0922 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -455,7 +455,7 @@ int main(int argc, char** argv) { RefusalCase(*device, block); if (evicting) { const auto kept = AgcDriver::Graphics::DepthPlaneCopyCount(); - Require(kept == 1, std::to_string(kept) + " depth plane copies were kept with a 1 MiB budget, expected only the newest"); + Require(kept >= 1 && kept < 8, std::to_string(kept) + " depth plane copies were kept with a 1 MiB budget, expected at least the newest and fewer than the 8 an unbounded cache keeps"); } std::puts("depth plane sampling tests passed"); return 0;