From 50fb1556cb005aa4dfaed57b4f8c07f2bb56e23c Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:03:55 -0500 Subject: vulkan: 32 MiB memory blocks and best-fit placement for images Textures created and deleted by the thousand still left about 1.7 GB of the 64 MiB blocks empty after ten minutes of title 1. Smaller blocks go back to the driver sooner, VMA gives allocations over 16 MiB their own memory, and the min-memory strategy fills holes before taking a block. (cherry picked from commit 6235081422f2bd2d726ccb45354c46d7c3ed2a0e) --- src/video_core/vulkan_common/vulkan_device.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src/video_core/vulkan_common/vulkan_device.cpp') diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index b65c721bb8..728005cacf 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -779,10 +779,13 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR .flags = flags, .physicalDevice = physical, .device = *logical, - // 64 MiB blocks on discrete GPUs too: with 256 MiB ones, Switch 2 titles' mix of + // 32 MiB blocks on discrete GPUs too: with 256 MiB ones, Switch 2 titles' mix of // texture sizes left ~38% of the image blocks empty (2 GB of an 8 GB card in title - // 1), which the texture cache counted as used and kept evicting textures for. - .preferredLargeHeapBlockSize = 64u * 1024u * 1024u, + // 1), which the texture cache counted as used and kept evicting textures for. With + // 64 MiB ones textures created and deleted by the thousand still left ~1.7 GB of holes + // after ten minutes; VMA also gives allocations over half a block (16 MiB) their own + // memory, which goes back to the driver when they are freed. + .preferredLargeHeapBlockSize = 32u * 1024u * 1024u, .pAllocationCallbacks = nullptr, .pDeviceMemoryCallbacks = nullptr, .pHeapSizeLimit = heap_size_limit != 0 ? heap_size_limits.data() : nullptr, -- cgit v1.2.3 From 1f0e3b88c4f82e61a6296eea9ee371eaf45676d4 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:41:57 -0500 Subject: vulkan: keep sparse pages and device-local buffers out of the image blocks About half of the holes in the default blocks sat next to buffers (~1,100, churned by the buffer cache's collector) or sparse pages (~2,400 64 KiB-4 MiB allocations), which the texture cache's collector can't move. Each gets a VMA pool of its own per memory type (32 MiB blocks); allocations over half a block, or ones the pool fails, take the default blocks. (cherry picked from commit 6c5ad324bd79ec50601ae14a63ca745da612f61f) --- src/video_core/vulkan_common/vulkan_device.cpp | 28 +++++++++++ src/video_core/vulkan_common/vulkan_device.h | 16 +++++++ .../vulkan_common/vulkan_memory_allocator.cpp | 55 +++++++++++++++++++++- .../vulkan_common/vulkan_memory_allocator.h | 11 +++++ 4 files changed, 109 insertions(+), 1 deletion(-) (limited to 'src/video_core/vulkan_common/vulkan_device.cpp') diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 728005cacf..bcf1743e11 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -807,9 +807,37 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR Device::~Device() { SaveStaticPipelineCache(); ShutdownGPULogging(); + for (auto& pools : separate_pools) { + for (VmaPool pool : pools) { + if (pool != VK_NULL_HANDLE) { + vmaDestroyPool(allocator, pool); + } + } + } vmaDestroyAllocator(allocator); } +VmaPool Device::GetSeparatePool(SeparatePool kind, u32 memory_type) const { + std::scoped_lock lk{separate_pools_mutex}; + VmaPool& pool = separate_pools[static_cast(kind)][memory_type]; + if (pool == VK_NULL_HANDLE) { + const VmaPoolCreateInfo ci{ + .memoryTypeIndex = memory_type, + .flags = 0, + .blockSize = SeparatePoolBlockSize, + .minBlockCount = 0, + .maxBlockCount = 0, + .priority = 0.f, + .minAllocationAlignment = 0, + .pMemoryAllocateNext = nullptr, + }; + if (vmaCreatePool(allocator, &ci, &pool) != VK_SUCCESS) { + pool = VK_NULL_HANDLE; + } + } + return pool; +} + void Device::LoadStaticPipelineCache() { const auto create = [this](size_t size, const void* data) { static_pipeline_cache = logical.CreatePipelineCache({ diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 8237868c6d..77e1dba4d3 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -6,8 +6,10 @@ #pragma once +#include #include #include +#include #include #include #include @@ -21,6 +23,7 @@ #include "video_core/vulkan_common/vulkan_wrapper.h" VK_DEFINE_HANDLE(VmaAllocator) +VK_DEFINE_HANDLE(VmaPool) // Define all features which may be used by the implementation here. // Vulkan version in the macro describes the minimum version required for feature availability. @@ -264,6 +267,17 @@ public: return allocator; } + /// Allocations kept out of the default blocks, which then hold images only: churned buffers + /// and long-lived sparse pages left holes the texture cache's collector couldn't drain. + enum class SeparatePool : u32 { + SparsePages, + DeviceBuffers, + }; + static constexpr VkDeviceSize SeparatePoolBlockSize = 32ULL << 20; + + /// The pool of that kind for a memory type, created on first use (null if that failed). + VmaPool GetSeparatePool(SeparatePool kind, u32 memory_type) const; + /// Returns the logical device. const vk::Device& GetLogical() const { return logical; @@ -1179,6 +1193,8 @@ private: private: VkInstance instance; ///< Vulkan instance. VmaAllocator allocator{}; ///< VMA allocator. + mutable std::mutex separate_pools_mutex; + mutable std::array, 2> separate_pools{}; vk::DeviceDispatch dld; ///< Device function pointers. vk::PhysicalDevice physical; ///< Physical device. vk::Device logical; ///< Logical device. diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 273e8414ce..79fb331de0 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -316,6 +316,24 @@ VmaAllocation MemoryAllocator::AllocateSparseMemory(const VkMemoryRequirements & }; VmaAllocation allocation{}; VmaAllocationInfo info{}; + // In a pool of their own (see Device::SeparatePool); anything over half a block, or a pool + // allocation that fails, takes the default blocks. + u32 memory_type{}; + if (requirements.size <= Device::SeparatePoolBlockSize / 2 && + vmaFindMemoryTypeIndex(allocator, requirements.memoryTypeBits, &alloc_ci, &memory_type) == + VK_SUCCESS) { + if (const VmaPool pool = + device.GetSeparatePool(Device::SeparatePool::SparsePages, memory_type)) { + VmaAllocationCreateInfo pool_ci = alloc_ci; + pool_ci.pool = pool; + if (vmaAllocateMemory(allocator, &requirements, &pool_ci, &allocation, &info) == + VK_SUCCESS) { + memory = info.deviceMemory; + offset = info.offset; + return allocation; + } + } + } if (vmaAllocateMemory(allocator, &requirements, &alloc_ci, &allocation, &info) != VK_SUCCESS) { return VK_NULL_HANDLE; } @@ -330,6 +348,39 @@ void MemoryAllocator::FreeSparseMemory(VmaAllocation allocation) const { } } +bool MemoryAllocator::CreateBufferInSeparatePool(const VkBufferCreateInfo& ci, MemoryUsage usage, + const VmaAllocationCreateInfo& alloc_ci, + VkBuffer& handle, VmaAllocation& allocation, + VmaAllocationInfo& alloc_info) const { + // The buffer cache's buffers, churned by its collector: in a pool of their own (see + // Device::SeparatePool). Anything over half a block takes the default blocks (dedicated). + if (usage != MemoryUsage::DeviceLocal || ci.size > Device::SeparatePoolBlockSize / 2) { + return false; + } + u32 memory_type{}; + { + std::scoped_lock lk{buffer_type_mutex}; + const auto key = (u64{ci.usage} << 32) | ci.flags; + if (const auto it = buffer_memory_types.find(key); it != buffer_memory_types.end()) { + memory_type = it->second; + } else if (vmaFindMemoryTypeIndexForBufferInfo(allocator, &ci, &alloc_ci, &memory_type) == + VK_SUCCESS) { + buffer_memory_types.emplace(key, memory_type); + } else { + return false; + } + } + const VmaPool pool = device.GetSeparatePool(Device::SeparatePool::DeviceBuffers, memory_type); + if (pool == VK_NULL_HANDLE) { + return false; + } + VmaAllocationCreateInfo pool_ci = alloc_ci; + pool_ci.pool = pool; + pool_ci.memoryTypeBits = 0; + return vmaCreateBuffer(allocator, &ci, &pool_ci, &handle, &allocation, &alloc_info) == + VK_SUCCESS; +} + vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const { // MESA will do memcpy() if not marked as host cached, so just force mark it for most buffers auto const anv_flags = (usage == MemoryUsage::Stream @@ -351,7 +402,9 @@ vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsa VmaAllocation allocation{}; VkMemoryPropertyFlags property_flags{}; - vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info)); + if (!CreateBufferInSeparatePool(ci, usage, alloc_ci, handle, allocation, alloc_info)) { + vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info)); + } vmaGetAllocationMemoryProperties(allocator, allocation, &property_flags); // Log GPU memory allocation for buffers diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index e1c267ab79..ffca933ee7 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -7,7 +7,9 @@ #pragma once #include +#include #include +#include #include #include "common/common_types.h" @@ -113,6 +115,12 @@ namespace Vulkan { vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased = false) const; /// Device memory to bind to part of a sparse resource. Null when there is none left. + /// Device-local buffers in their own VMA pool; false when it doesn't apply or failed. + bool CreateBufferInSeparatePool(const VkBufferCreateInfo& ci, MemoryUsage usage, + const VmaAllocationCreateInfo& alloc_ci, VkBuffer& handle, + VmaAllocation& allocation, + VmaAllocationInfo& alloc_info) const; + VmaAllocation AllocateSparseMemory(const VkMemoryRequirements &requirements, VkDeviceMemory &memory, VkDeviceSize &offset) const; @@ -153,6 +161,9 @@ namespace Vulkan { const VkPhysicalDeviceMemoryProperties properties; ///< Physical device memory properties. VkDeviceSize buffer_image_granularity; ///< Adjacent buffer/image granularity u32 valid_memory_types{~0u}; + mutable std::mutex buffer_type_mutex; + /// Memory type of device-local buffers by usage and flags (CreateBufferInSeparatePool). + mutable std::unordered_map buffer_memory_types; }; /// Logs VMA's blocks and allocations per memory type, and each heap's budget. -- cgit v1.2.3 From a08eb2ead837b8de5dbd4c666474ee62ea45bd68 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:42:38 -0500 Subject: vulkan: 16 MiB memory blocks With 32 MiB blocks title 1 still had ~0.5 GB of holes between images: a block drains only once all of its images are gone, and fewer per block drain sooner. Allocations over 8 MiB get memory of their own. (cherry picked from commit d3a4db818512221e6b0555e886e192c346c16ae7) --- src/video_core/vulkan_common/vulkan_device.cpp | 9 +++++---- src/video_core/vulkan_common/vulkan_device.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src/video_core/vulkan_common/vulkan_device.cpp') diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index bcf1743e11..10c9e501bf 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -779,13 +779,14 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR .flags = flags, .physicalDevice = physical, .device = *logical, - // 32 MiB blocks on discrete GPUs too: with 256 MiB ones, Switch 2 titles' mix of + // 16 MiB blocks on discrete GPUs too: with 256 MiB ones, Switch 2 titles' mix of // texture sizes left ~38% of the image blocks empty (2 GB of an 8 GB card in title // 1), which the texture cache counted as used and kept evicting textures for. With // 64 MiB ones textures created and deleted by the thousand still left ~1.7 GB of holes - // after ten minutes; VMA also gives allocations over half a block (16 MiB) their own - // memory, which goes back to the driver when they are freed. - .preferredLargeHeapBlockSize = 32u * 1024u * 1024u, + // after ten minutes, with 32 MiB ones ~0.5 GB: a block drains only once all of its + // images are gone, and fewer per block drain sooner. VMA gives allocations over half a + // block (8 MiB) their own memory, which goes back to the driver when they are freed. + .preferredLargeHeapBlockSize = 16u * 1024u * 1024u, .pAllocationCallbacks = nullptr, .pDeviceMemoryCallbacks = nullptr, .pHeapSizeLimit = heap_size_limit != 0 ? heap_size_limits.data() : nullptr, diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 77e1dba4d3..5c757bedc4 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -273,7 +273,7 @@ public: SparsePages, DeviceBuffers, }; - static constexpr VkDeviceSize SeparatePoolBlockSize = 32ULL << 20; + static constexpr VkDeviceSize SeparatePoolBlockSize = 16ULL << 20; /// The pool of that kind for a memory type, created on first use (null if that failed). VmaPool GetSeparatePool(SeparatePool kind, u32 memory_type) const; -- cgit v1.2.3