diff options
| author | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:01:57 +0100 |
|---|---|---|
| committer | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:01:57 +0100 |
| commit | b374709aa202d586dd09f81f3389da8d73fea0d7 (patch) | |
| tree | 5c4033d827c517aca9cc73dc497fbb3e81291bf1 /src/video_core/vulkan_common | |
| parent | 021c9724371e45fa694c9ee3106b9011364d2853 (diff) | |
| parent | ce1b1de6f1beed36958368f89cda466937ff9716 (diff) | |
Merge remote-tracking branch 'origin/nx2' into codex/upstream-loading-hotkeys
Diffstat (limited to 'src/video_core/vulkan_common')
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_device.cpp | 57 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_device.h | 36 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_memory_allocator.cpp | 81 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_memory_allocator.h | 19 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_wrapper.cpp | 14 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_wrapper.h | 14 |
6 files changed, 215 insertions, 6 deletions
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index b65c721bb8..01e13cb384 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -779,10 +779,14 @@ 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 + // 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. - .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, 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, @@ -804,9 +808,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<u32>(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({ @@ -1515,6 +1547,15 @@ void Device::RemoveUnsuitableExtensions() { VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME); } + // VK_EXT_mesh_shader: Switch 2 titles draw some geometry with task and mesh programs. Only + // the two stages are used; the multiview, shading rate and query features need more. + features.mesh_shader.multiviewMeshShader = VK_FALSE; + features.mesh_shader.primitiveFragmentShadingRateMeshShader = VK_FALSE; + features.mesh_shader.meshShaderQueries = VK_FALSE; + extensions.mesh_shader = features.mesh_shader.taskShader && features.mesh_shader.meshShader; + RemoveExtensionFeatureIfUnsuitable(extensions.mesh_shader, features.mesh_shader, + VK_EXT_MESH_SHADER_EXTENSION_NAME); + // VK_KHR_shader_quad_control extensions.shader_quad_control = features.shader_quad_control.shaderQuadControl; RemoveExtensionFeatureIfUnsuitable(extensions.shader_quad_control, features.shader_quad_control, @@ -1643,6 +1684,16 @@ u64 Device::GetDeviceMemoryUsage() const { return result; } +u64 Device::GetDeviceMemorySlack() const { + if (allocator == nullptr) { + return 0; + } + std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; + vmaGetHeapBudgets(allocator, heap_budgets.data()); + const VmaStatistics& statistics = heap_budgets[main_heap].statistics; + return statistics.blockBytes - (std::min)(statistics.blockBytes, statistics.allocationBytes); +} + u64 Device::GetDeviceMemoryBudget() const { if (!extensions.memory_budget) { return 0; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 8237868c6d..622d792210 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 <array> #include <atomic> #include <cstdlib> +#include <mutex> #include <optional> #include <set> #include <span> @@ -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. @@ -66,6 +69,7 @@ VK_DEFINE_HANDLE(VmaAllocator) FEATURE(EXT, 4444Formats, 4444_FORMATS, format_a4b4g4r4) \ FEATURE(EXT, IndexTypeUint8, INDEX_TYPE_UINT8, index_type_uint8) \ FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \ + FEATURE(EXT, MeshShader, MESH_SHADER, mesh_shader) \ FEATURE(EXT, PrimitiveTopologyListRestart, PRIMITIVE_TOPOLOGY_LIST_RESTART, \ primitive_topology_list_restart) \ FEATURE(EXT, ProvokingVertex, PROVOKING_VERTEX, provoking_vertex) \ @@ -264,6 +268,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 = 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; + /// Returns the logical device. const vk::Device& GetLogical() const { return logical; @@ -633,6 +648,12 @@ FN_MAX_LIMIT_LIST return extensions.shader_quad_control && features.shader_quad_control.shaderQuadControl; } + /// Returns true if the device supports task and mesh shaders (VK_EXT_mesh_shader). + bool IsExtMeshShaderSupported() const { + return extensions.mesh_shader && features.mesh_shader.taskShader && + features.mesh_shader.meshShader; + } + /// Returns true if the device supports VK_KHR_image_format_list. bool IsKhrImageFormatListSupported() const { return extensions.image_format_list || instance_version >= VK_API_VERSION_1_2; @@ -951,6 +972,11 @@ FN_MAX_LIMIT_LIST return 0x00010300U; } + /// Returns true for an integrated GPU (its device-local memory is system memory). + bool IsIntegrated() const { + return is_integrated; + } + /// Returns true when a known debugging tool is attached. bool HasDebuggingToolAttached() const { return has_renderdoc || has_nsight_graphics || has_radeon_gpu_profiler; @@ -993,9 +1019,15 @@ FN_MAX_LIMIT_LIST /// The driver's current memory budget for this process (VK_EXT_memory_budget), in /// GetDeviceMemoryUsage() terms: the usage at which the main device-local heap reaches its - /// budget. 0 without the extension. + /// budget. 0 without the extension. The unused part of the allocator's blocks is not + /// subtracted: see GetDeviceMemorySlack(). u64 GetDeviceMemoryBudget() const; + /// Memory the allocator holds from the driver in the main device-local heap but has no + /// allocation in (the unused part of its blocks). GetDeviceMemoryUsage() does not count it, + /// yet the driver does: with the heap at its budget it is not available to new images. + u64 GetDeviceMemorySlack() const; + u32 GetSetsPerPool() const { return sets_per_pool; } @@ -1179,6 +1211,8 @@ private: private: VkInstance instance; ///< Vulkan instance. VmaAllocator allocator{}; ///< VMA allocator. + mutable std::mutex separate_pools_mutex; + mutable std::array<std::array<VmaPool, VK_MAX_MEMORY_TYPES>, 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 1798611e26..a0a39abe39 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -209,7 +209,10 @@ MemoryAllocator::~MemoryAllocator() = default; vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicated) const { const VmaAllocationCreateInfo alloc_ci = { - .flags = static_cast<VmaAllocationCreateFlags>(VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT) | + // Best fit: images freed by the thousand leave holes in the blocks, which new ones + // should fill before VMA takes another block. + .flags = static_cast<VmaAllocationCreateFlags>(VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | + VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT) | (dedicated ? VmaAllocationCreateFlags{VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT} : VmaAllocationCreateFlags{0}), .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, @@ -242,11 +245,32 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels, ci.arrayLayers, static_cast<u32>(ci.format)); } + pressure_events.fetch_add(1, std::memory_order_relaxed); VmaAllocationCreateInfo relaxed = alloc_ci; relaxed.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT; result = vmaCreateImage(allocator, &ci, &relaxed, &handle, &allocation, &alloc_info); } + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && !huge) { + // Still out of device memory: a real image in host memory (slow, but valid) beats one + // with nothing bound, which AMD's drivers do not survive being sampled. + VmaAllocationCreateInfo host = alloc_ci; + host.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT; + host.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + host.preferredFlags = 0; + result = vmaCreateImage(allocator, &ci, &host, &handle, &allocation, &alloc_info); + static u32 host_logged{}; + if (result == VK_SUCCESS && host_logged++ < 16) { + LOG_WARNING(Render_Vulkan, + "Image {}x{}x{} ({} levels, {} layers, format {}) is out of device memory; " + "placed in host memory", + ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels, + ci.arrayLayers, static_cast<u32>(ci.format)); + } + } if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && ci.imageType != VK_IMAGE_TYPE_1D) { + if (!huge) { + pressure_events.fetch_add(1, std::memory_order_relaxed); + } // Too big to back at all, e.g. title 1's sparse virtual texture arrays (8192x8192, // 68 layers, ~23 GB): on the console only resident tiles have memory. Use an image with // nothing bound, so views and copies still work, instead of aborting. @@ -313,6 +337,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; } @@ -327,6 +369,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 @@ -348,7 +423,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..8b9e72b3e3 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -6,8 +6,11 @@ #pragma once +#include <atomic> #include <memory> +#include <mutex> #include <span> +#include <unordered_map> #include <vector> #include "common/common_types.h" @@ -113,11 +116,23 @@ 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; void FreeSparseMemory(VmaAllocation allocation) const; + /// Counts the images that did not fit the device memory budget (made anyway, in host + /// memory or with nothing bound): the texture cache collects harder when it grows. + u32 MemoryPressureEvents() const noexcept { + return pressure_events.load(std::memory_order_relaxed); + } + vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const; vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage, @@ -153,6 +168,10 @@ namespace Vulkan { const VkPhysicalDeviceMemoryProperties properties; ///< Physical device memory properties. VkDeviceSize buffer_image_granularity; ///< Adjacent buffer/image granularity u32 valid_memory_types{~0u}; + mutable std::atomic<u32> pressure_events{0}; + mutable std::mutex buffer_type_mutex; + /// Memory type of device-local buffers by usage and flags (CreateBufferInSeparatePool). + mutable std::unordered_map<u64, u32> buffer_memory_types; }; /// Logs VMA's blocks and allocations per memory type, and each heap's budget. diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index dbaaf450a0..d0bca88125 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -116,6 +116,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkCmdDrawIndirectCount); X(vkCmdDrawIndexedIndirectCount); X(vkCmdDrawIndirectByteCountEXT); + X(vkCmdDrawMeshTasksEXT); X(vkCmdEndConditionalRenderingEXT); X(vkCmdEndQuery); X(vkCmdEndRenderPass); @@ -525,6 +526,19 @@ void Image::Release() const noexcept { } } +Image::MemoryBlock Image::GetMemoryBlock() const noexcept { + if (allocation == nullptr) { + return {}; + } + VmaAllocationInfo2 info{}; + vmaGetAllocationInfo2(allocator, allocation, &info); + if (info.dedicatedMemory) { + return {0, info.blockSize, info.allocationInfo.size}; + } + return {reinterpret_cast<u64>(info.allocationInfo.deviceMemory), info.blockSize, + info.allocationInfo.size}; +} + void Buffer::Flush() const { if (!is_coherent) { vmaFlushAllocation(allocator, allocation, 0, VK_WHOLE_SIZE); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 3010368973..f49e9518bb 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -232,6 +232,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkCmdDrawIndirectCount vkCmdDrawIndirectCount{}; PFN_vkCmdDrawIndexedIndirectCount vkCmdDrawIndexedIndirectCount{}; PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT{}; + PFN_vkCmdDrawMeshTasksEXT vkCmdDrawMeshTasksEXT{}; PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT{}; PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT{}; PFN_vkCmdEndQuery vkCmdEndQuery{}; @@ -736,6 +737,15 @@ public: return usage; } + /// Where the image's memory lies: the block (0 when the image has no memory or a block of + /// its own), the block's size and the image's part of it. + struct MemoryBlock { + u64 block; + u64 block_size; + u64 size; + }; + [[nodiscard]] MemoryBlock GetMemoryBlock() const noexcept; + private: void Release() const noexcept; @@ -1319,6 +1329,10 @@ public: dld->vkCmdDrawIndexedIndirect(handle, src_buffer, src_offset, draw_count, stride); } + void DrawMeshTasksEXT(u32 group_count_x, u32 group_count_y, u32 group_count_z) const noexcept { + dld->vkCmdDrawMeshTasksEXT(handle, group_count_x, group_count_y, group_count_z); + } + void DrawIndirectCount(VkBuffer src_buffer, VkDeviceSize src_offset, VkBuffer count_buffer, VkDeviceSize count_offset, u32 draw_count, u32 stride) const noexcept { dld->vkCmdDrawIndirectCount(handle, src_buffer, src_offset, count_buffer, count_offset, |
