aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/vulkan_common
diff options
context:
space:
mode:
authorkraibse <33869366+kraibse@users.noreply.github.com>2026-11-02 16:01:57 +0100
committerkraibse <33869366+kraibse@users.noreply.github.com>2026-11-02 16:01:57 +0100
commitb374709aa202d586dd09f81f3389da8d73fea0d7 (patch)
tree5c4033d827c517aca9cc73dc497fbb3e81291bf1 /src/video_core/vulkan_common
parent021c9724371e45fa694c9ee3106b9011364d2853 (diff)
parentce1b1de6f1beed36958368f89cda466937ff9716 (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.cpp57
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h36
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp81
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h19
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.cpp14
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h14
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,