From 3bb4494ea7a23f95d697b884d3aaa21913e11345 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 7 Oct 2026 16:15:28 +0200 Subject: [PATCH] perf(agc): size the device staging pool from the device-local heap The buffer pool's device tier (detiler scratch buffers and the staging shadows of written guest buffers) retained at most 512 MiB by default. A title whose working set of these buffers is larger allocates and frees device memory continually: PPSA14632 (LEGO Horizon Adventures) had 70761 misses and 69828 evictions per 10 s with 485 of 512 MiB retained, each a vkAllocateMemory or vkFreeMemory. The default budget is now an eighth of the largest device-local heap, and at least the old 512 MiB, so a 16 GiB card retains up to 2 GiB and an 8 GiB card 1 GiB. APS5_STAGING_POOL_MIB still sets it. Only the constructor knows the device, so only the budget comes from the heap. Whether the tier is in use at all (APS5_STAGING_POOL_MIB unset or not 0) stays a static check, DeviceTierEnabled, which Capacity, Usage and tierFor share, so the size classes and the tier stay consistent. On PPSA14632 with 2 GiB the device tier has 1312 misses and 99 evictions per 10 s (1628 MiB retained), and presents rise from about 34 to about 39 per 10 s. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/BufferPool.hpp | 9 ++------ .../Graphics/src/BufferPool.cpp | 22 ++++++++++++++----- .../prx/libSceAgcDriver/tests/Graphics.cpp | 15 +++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index f73ee10c2..5b4262f0b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -43,14 +43,10 @@ struct BufferAllocation { // the 256-byte copied-region buffers to fit, and those then missed on every build (81869 misses // and 80850 evictions per run, each a Vulkan create or destroy under the pool mutex, 3.5 s per // run). APS5_BUFFER_POOL_SHARED=1 keeps one tier as before. -// -// Device-local allocations (the detiler's scratch buffers and the staging shadows of written guest -// buffers, see GuestBufferMemory) are retained in a third tier with a budget of their own, video -// memory instead of pinned host memory: APS5_STAGING_POOL_MIB (default 512); 0 keeps them in the -// two host tiers as before. class BufferPool { public: explicit BufferPool(const Context& context); + static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory); ~BufferPool(); BufferPool(const BufferPool&) = delete; BufferPool& operator=(const BufferPool&) = delete; @@ -91,8 +87,7 @@ private: // The tier a buffer of `capacity` and `properties` is retained in (the large one for everything // when shared; the device tier for device-local memory while it has a budget). Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties); - // The device tier's budget (APS5_STAGING_POOL_MIB), read once. - static VkDeviceSize DeviceBudget(); + static bool DeviceTierEnabled(); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); void destroy(const BufferAllocation& allocation) noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index d466793d3..b3df4227e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -29,7 +29,7 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) { smallTier.budget = smallBudget; largeTier.budget = budget; - deviceTier.budget = DeviceBudget(); + deviceTier.budget = DeviceTierEnabled() ? DeviceBudget(context.memory) : 0; } BufferPool::~BufferPool() { @@ -40,12 +40,22 @@ BufferPool::~BufferPool() { } } -VkDeviceSize BufferPool::DeviceBudget() { - static const VkDeviceSize deviceBudget = [] { +bool BufferPool::DeviceTierEnabled() { + static const bool enabled = [] { const char* value = std::getenv("APS5_STAGING_POOL_MIB"); - return (value != nullptr ? std::strtoull(value, nullptr, 10) : 512ull) << 20u; + return value == nullptr || std::strtoull(value, nullptr, 10) != 0; }(); - return deviceBudget; + return enabled; +} + +VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory) { + static const char* value = std::getenv("APS5_STAGING_POOL_MIB"); + if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; + VkDeviceSize largest = 0; + for (std::uint32_t heap = 0; heap < memory.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) largest = std::max(largest, memory.memoryHeaps[heap].size); + } + return std::max(VkDeviceSize{512} << 20u, largest / 8u); } void BufferPool::destroy(const BufferAllocation& allocation) noexcept { @@ -56,7 +66,7 @@ void BufferPool::destroy(const BufferAllocation& allocation) noexcept { } bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) { - return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceBudget() != 0; + return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceTierEnabled(); } std::size_t BufferPool::Capacity(std::size_t bytes, VkMemoryPropertyFlags properties) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c6e824c7b..400b62a80 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -3549,6 +3550,19 @@ void meshIndexBufferTests() { expectFailure([] { static_cast(AgcDriver::Graphics::MeshIndexBufferDescriptor(AgcDriver::Pm4::DrawParameters{0, 3, 2, 1, 0, true})); }, "invalid mesh index buffer range"); } +void stagingPoolBudgetTests() { + if (std::getenv("APS5_STAGING_POOL_MIB") != nullptr) return; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 2; + memory.memoryHeaps[0] = {VkDeviceSize{32} << 30u, 0}; + memory.memoryHeaps[1] = {VkDeviceSize{16} << 30u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{2} << 30u, "the staging pool budget is not an eighth of the device-local heap"); + memory.memoryHeaps[1].size = VkDeviceSize{2} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "the staging pool budget fell below 512 MiB"); + memory.memoryHeapCount = 1; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a host heap sized the staging pool budget"); +} + void meshArgumentTests() { using AgcDriver::Graphics::MeshArguments; using AgcDriver::Graphics::ResolveMeshArguments; @@ -3747,6 +3761,7 @@ int main() { sampledBudgetReportTests(); meshArgumentTests(); meshIndexBufferTests(); + stagingPoolBudgetTests(); validationTests(); vertexCopyTests(); highestDrawIndexTests();