From 39b9892f000b3ae943d3dd72a8729eaf71db0595 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:49:31 -0400 Subject: vulkan_device: keep an integrated GPU's budget within its memory share CollectPhysicalMemoryInfo gives an integrated GPU 4 GiB of device memory (6 GiB with aggressive VRAM use), since its device-local heap is system memory. GetDeviceMemoryBudget returned the heap's driver budget instead (21178 MiB on a Radeon 780M), and the texture cache follows that budget for Switch 2 titles: it collected only past 20.6 GB, filling system RAM, and kept ASTC uncompressed (the BC3 switch looks at the budget too). Cap it at that share. Radeon 780M, Mario Kart World title screen, 200 s: shared GPU memory 2.7 -> 3.6 GB instead of 7.6 -> 12.7 GB (0.25 instead of 1.8 GB/min), private bytes 12.9 instead of 22.4 GB at the end, ASTC re-compressed to BC3, same frame rate. --- src/video_core/vulkan_common/vulkan_device.cpp | 7 +++++++ 1 file changed, 7 insertions(+) (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 623ab7c30d..c1a40e7906 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1663,6 +1663,13 @@ u64 Device::GetDeviceMemoryBudget() const { result += AllocatedUsage(budget.heapUsage[heap], heap_budgets[heap]); } } + if (is_integrated) { + // An integrated GPU's device-local heap is system memory (a Radeon 780M reported a 21 GB + // budget): following it let the texture cache fill that much RAM before collecting, and + // kept ASTC uncompressed. Its budget is at most the share CollectPhysicalMemoryInfo + // gives it (4 GiB, 6 GiB with aggressive VRAM use). + result = (std::min)(result, device_access_memory); + } return result; } -- cgit v1.2.3 From fce9c5fd25c66558eb8ed4e9e5ca9129589a5e7d Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:08:04 -0500 Subject: vulkan_device: tell AMD GCN4 apart before float16 is turned off AMD GCN4 and older are recognised by their lack of float16. Since float16 is turned off for every non-Mesa AMD driver (2026+ versions break it), the sampler min/max check that runs after that saw every AMD GPU as GCN4 and removed VK_EXT_sampler_filter_minmax: a Radeon 780M (RDNA3) logged "AMD GCN4 and earlier have broken VK_EXT_sampler_filter_minmax", and samplers with a min/max reduction fell back to plain filtering there. Read it once, before float16 is turned off. --- src/video_core/vulkan_common/vulkan_device.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 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 c1a40e7906..b65c721bb8 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -620,12 +620,16 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR } sets_per_pool = 64; + // AMD GCN4 and older are told apart by their lack of float16, read before float16 is turned + // off for newer AMD drivers below: checked after that, every AMD GPU looked like GCN4 and lost + // VK_EXT_sampler_filter_minmax (an RDNA3 Radeon 780M logged it). + const bool amd_gcn4_or_older = is_amd && !features.shader_float16_int8.shaderFloat16; if (is_amd_driver) { // AMD drivers need a higher amount of Sets per Pool in certain circumstances like in XC2. sets_per_pool = 96; // Disable VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT on AMD GCN4 and lower as it is broken. - if (!features.shader_float16_int8.shaderFloat16) { + if (amd_gcn4_or_older) { LOG_WARNING(Render_Vulkan, "AMD GCN4 and earlier have broken VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT"); has_broken_cube_compatibility = true; @@ -641,7 +645,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR if (extensions.sampler_filter_minmax && is_amd) { // Disable ext_sampler_filter_minmax on AMD GCN4 and lower as it is broken. - if (!features.shader_float16_int8.shaderFloat16) { + if (amd_gcn4_or_older) { LOG_WARNING(Render_Vulkan, "AMD GCN4 and earlier have broken VK_EXT_sampler_filter_minmax"); RemoveExtension(extensions.sampler_filter_minmax, -- cgit v1.2.3 From 02504aeb37cb3ee07dc421acc0570ad5ba25d53f Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:02:52 -0400 Subject: video_core: draw Switch 2 task/mesh pipelines with VK_EXT_mesh_shader While SET_MESH_CONTROL is on, a native draw binds a task program in the vertex slot, its mesh program in the tessellation slot and a header-only pass-through geometry program naming the per-primitive outputs. Translate the first two as task and mesh programs with the mesh registers (SET_MESH_SHADER_A/B, SET_MESH_INIT_SHADER) and that header, key their pipelines by them, and build pipelines with task/mesh stages and no vertex input, input assembly or tessellation state. The draw's vertex count is the number of task workgroups: record vkCmdDrawMeshTasksEXT. Enable VK_EXT_mesh_shader (task and mesh only) when the host has it; draws are skipped without it, as they were before. --- src/video_core/dirty_flags.cpp | 2 ++ src/video_core/engines/maxwell_3d.h | 13 ++++++++ src/video_core/renderer_vulkan/maxwell_to_vk.cpp | 4 +++ .../renderer_vulkan/vk_graphics_pipeline.cpp | 35 ++++++++++++++++++---- .../renderer_vulkan/vk_graphics_pipeline.h | 5 ++++ .../renderer_vulkan/vk_pipeline_cache.cpp | 15 +++++++++- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 20 +++++++++++++ src/video_core/renderer_vulkan/vk_rasterizer.h | 1 + src/video_core/shader_cache.cpp | 27 +++++++++++++++++ src/video_core/shader_cache.h | 2 ++ src/video_core/shader_environment.cpp | 25 ++++++++++++++-- src/video_core/vulkan_common/vulkan_device.cpp | 9 ++++++ src/video_core/vulkan_common/vulkan_device.h | 7 +++++ src/video_core/vulkan_common/vulkan_wrapper.cpp | 1 + src/video_core/vulkan_common/vulkan_wrapper.h | 5 ++++ 15 files changed, 163 insertions(+), 8 deletions(-) (limited to 'src/video_core/vulkan_common/vulkan_device.cpp') diff --git a/src/video_core/dirty_flags.cpp b/src/video_core/dirty_flags.cpp index e6856aaca3..2f359749aa 100644 --- a/src/video_core/dirty_flags.cpp +++ b/src/video_core/dirty_flags.cpp @@ -64,6 +64,8 @@ void SetupDirtyRenderTargets(Maxwell3D::DirtyState::Tables& tables) { void SetupDirtyShaders(Maxwell3D::DirtyState::Tables& tables) { FillBlock(tables[0], OFF(pipelines), NUM(pipelines[0]) * Maxwell3D::Regs::MaxShaderProgram, Shaders); + // Task and mesh programs are translated with the mesh registers. + FillBlock(tables[1], Maxwell3D::Regs::MeshControl, 4, Shaders); } } // Anonymous namespace diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 0121f1a01c..3e946db1fe 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -2541,6 +2541,19 @@ public: return IsShaderConfigEnabled(static_cast(type)); } + // Switch 2 (NVN) task and mesh pipelines (clc597.h): SET_MESH_CONTROL, SET_MESH_SHADER_A + // and _B, SET_MESH_INIT_SHADER. While mesh control is on, the vertex slot holds the task + // program (or the mesh program when there is no task), the tessellation slot the mesh + // program and the geometry slot a pass-through header naming the per-primitive outputs. + static constexpr std::size_t MeshControl = 0x114c / 4; + static constexpr std::size_t MeshShaderA = 0x1150 / 4; + static constexpr std::size_t MeshShaderB = 0x1154 / 4; + static constexpr std::size_t MeshInitShader = 0x1158 / 4; + + bool IsMeshEnabled() const { + return (reg_array[MeshControl] & 1) != 0; + } + struct ConstantBuffer { u32 size; u32 address_high; diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index c8e43d0650..e03e5786fe 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -333,6 +333,10 @@ VkShaderStageFlagBits ShaderStage(Shader::Stage stage) { return VK_SHADER_STAGE_FRAGMENT_BIT; case Shader::Stage::Compute: return VK_SHADER_STAGE_COMPUTE_BIT; + case Shader::Stage::Task: + return VK_SHADER_STAGE_TASK_BIT_EXT; + case Shader::Stage::Mesh: + return VK_SHADER_STAGE_MESH_BIT_EXT; } UNIMPLEMENTED_MSG("Unimplemented shader stage={}", stage); return {}; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 94bace432c..86b6e5a983 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -54,6 +54,17 @@ using VideoCore::Surface::PixelFormatFromRenderTargetFormat; constexpr size_t NUM_STAGES = Maxwell::MaxShaderStage; +// The stage of a program: task and mesh programs sit in the vertex and tessellation slots. +VkShaderStageFlagBits StageFlag(const Shader::Info& info, VkShaderStageFlagBits slot_stage) { + if (info.is_task) { + return VK_SHADER_STAGE_TASK_BIT_EXT; + } + if (info.is_mesh) { + return VK_SHADER_STAGE_MESH_BIT_EXT; + } + return slot_stage; +} + DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span infos) { DescriptorLayoutBuilder builder{device}; for (size_t index = 0; index < infos.size(); ++index) { @@ -64,7 +75,7 @@ DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span(shader_stages.size()), .pStages = shader_stages.data(), - .pVertexInputState = &vertex_input_ci, - .pInputAssemblyState = &input_assembly_ci, - .pTessellationState = &tessellation_ci, + .pVertexInputState = is_mesh ? nullptr : &vertex_input_ci, + .pInputAssemblyState = is_mesh ? nullptr : &input_assembly_ci, + .pTessellationState = is_mesh ? nullptr : &tessellation_ci, .pViewportState = &viewport_ci, .pRasterizationState = &rasterization_ci, .pMultisampleState = &multisample_ci, diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index b1ac5a3fcc..daee7910c1 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -98,6 +98,11 @@ public: bool UsesExtendedDynamicState() const noexcept { return key.state.extended_dynamic_state != 0; } + + /// Switch 2 task/mesh pipeline: drawn with vkCmdDrawMeshTasksEXT. + [[nodiscard]] bool IsMeshPipeline() const noexcept { + return stage_infos[0].is_task || stage_infos[0].is_mesh; + } GraphicsPipeline& operator=(GraphicsPipeline&&) noexcept = delete; GraphicsPipeline(GraphicsPipeline&&) noexcept = delete; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 2c64f3375b..03f390d272 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -170,6 +170,14 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program } else { info.previous_stage_stores.mask.set(); } + if (previous_program && previous_program->stage == Shader::Stage::Mesh) { + for (const u32 line : previous_program->mesh.primitive_lines) { + const auto attribute{static_cast(line)}; + if (Shader::IR::IsGeneric(attribute)) { + info.per_primitive_generics.set(Shader::IR::GenericAttributeIndex(attribute)); + } + } + } const Shader::Stage stage{program.stage}; const bool has_geometry{key.unique_hashes[4] != 0 && !programs[4].is_geometry_passthrough}; const bool gl_ndc{key.state.ndc_minus_one_to_one != 0}; @@ -230,6 +238,9 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program return Shader::TessSpacing::Equal; }(); break; + case Shader::Stage::Mesh: + info.convert_depth_mode = gl_ndc; + break; case Shader::Stage::Geometry: if (program.output_topology == Shader::OutputTopology::PointList) { info.fixed_state_point_size = point_size; @@ -374,7 +385,9 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Shader::Stage::TessellationEval) | subgroup_stage_bit(VK_SHADER_STAGE_GEOMETRY_BIT, Shader::Stage::Geometry) | subgroup_stage_bit(VK_SHADER_STAGE_FRAGMENT_BIT, Shader::Stage::Fragment) | - subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute)}; + subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute) | + subgroup_stage_bit(VK_SHADER_STAGE_TASK_BIT_EXT, Shader::Stage::Task) | + subgroup_stage_bit(VK_SHADER_STAGE_MESH_BIT_EXT, Shader::Stage::Mesh)}; if (driver_id == VK_DRIVER_ID_MESA_TURNIP) { supported_subgroup_stages &= ~(1u << static_cast(Shader::Stage::Geometry)); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index cb31a0e7e4..0109117344 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -279,6 +279,12 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { report_failure("no pipeline"); return; } + // Task/mesh pipelines: the draw's vertex count is the number of task workgroups. + mesh_draw = pipeline->IsMeshPipeline(); + if (mesh_draw && !device.IsExtMeshShaderSupported()) { + report_failure("no VK_EXT_mesh_shader"); + return; + } std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; // update engine as channel may be different. pipeline->SetEngine(maxwell3d, gpu_memory); @@ -541,6 +547,12 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { const u32 num_instances{instance_count}; const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)}; + if (mesh_draw) { + scheduler.Record([groups = draw_params.num_vertices](vk::CommandBuffer cmdbuf) { + cmdbuf.DrawMeshTasksEXT(groups, 1, 1); + }); + return; + } scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) { if (draw_params.is_indexed) { cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances, @@ -573,6 +585,14 @@ void RasterizerVulkan::DrawIndirect() { const auto& params = maxwell3d->draw_manager.indirect_state; buffer_cache.SetDrawIndirect(¶ms); PrepareDraw(params.is_indexed, [this, ¶ms] { + if (mesh_draw) { + // Indirect task/mesh draws would need vkCmdDrawMeshTasksIndirectEXT's layout. + static bool logged{}; + if (!std::exchange(logged, true)) { + LOG_WARNING(Render_Vulkan, "Indirect task/mesh draw skipped"); + } + return; + } const auto indirect_buffer = buffer_cache.GetDrawIndirectBuffer(); const auto& buffer = indirect_buffer.first; const auto& offset = indirect_buffer.second; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index ae9929ca0b..e8989dcdfc 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -230,6 +230,7 @@ private: boost::container::static_vector sampler_handles; u32 draw_counter = 0; + bool mesh_draw{}; ///< the draw being prepared uses a task/mesh pipeline }; } // namespace Vulkan diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index dfe80abac3..f03df6bac5 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -14,6 +14,7 @@ #include #include "common/assert.h" +#include "common/cityhash.h" #include "shader_recompiler/frontend/maxwell/control_flow.h" #include "shader_recompiler/exception.h" #include "shader_recompiler/object_pool.h" @@ -48,6 +49,22 @@ void ShaderCache::SyncGuestHost() { ShaderCache::ShaderCache(Tegra::MaxwellDeviceMemoryManager& device_memory_) : device_memory{device_memory_} {} +// Task and mesh programs are translated with the mesh registers and the header of the pass-through +// geometry program (per-primitive outputs), so pipelines are keyed by them too. +u64 ShaderCache::MeshStateHash() const { + using Regs = Tegra::Engines::Maxwell3D::Regs; + const auto& regs{maxwell3d->regs}; + std::array state{regs.reg_array[Regs::MeshShaderA], regs.reg_array[Regs::MeshShaderB], + regs.reg_array[Regs::MeshInitShader]}; + if (regs.IsShaderConfigEnabled(Regs::ShaderType::Geometry)) { + const auto& geometry{regs.pipelines[static_cast(Regs::ShaderType::Geometry)]}; + const GPUVAddr address{(static_cast(geometry.reserved[0] & 0xff) << 32) | + geometry.reserved[1]}; + gpu_memory->ReadBlockUnsafe(address, state.data() + 3, 20 * sizeof(u32)); + } + return Common::CityHash64(reinterpret_cast(state.data()), sizeof(state)) | 1; +} + bool ShaderCache::RefreshStages(std::array& unique_hashes) try { auto& dirty{maxwell3d->dirty.flags}; if (!dirty[VideoCommon::Dirty::Shaders]) { @@ -57,6 +74,7 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { const GPUVAddr base_addr{maxwell3d->regs.program_region.Address()}; const bool native = maxwell3d->GetNativeMacroEngine().native_turing; + const bool mesh{native && maxwell3d->regs.IsMeshEnabled()}; for (size_t index = 0; index < Tegra::Engines::Maxwell3D::Regs::MaxShaderProgram; ++index) { if (!maxwell3d->regs.IsShaderConfigEnabled(index)) { unique_hashes[index] = 0; @@ -69,6 +87,11 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { unique_hashes[index] = 0; continue; } + if (mesh && program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Geometry) { + // A header without code: the mesh program's environment reads its output map. + unique_hashes[index] = 0; + continue; + } const GPUVAddr shader_addr{native ? (static_cast(shader_config.reserved[0] & 0xff) << 32) | shader_config.reserved[1] : base_addr + shader_config.offset}; @@ -111,6 +134,10 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { } shader_infos[index] = shader_info; unique_hashes[index] = shader_info->unique_hash; + if (mesh && (program == Tegra::Engines::Maxwell3D::Regs::ShaderType::VertexB || + program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Tessellation)) { + unique_hashes[index] ^= MeshStateHash(); + } } last_shaders_valid = true; return true; diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index f4179dd435..80e375a38b 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -151,6 +151,8 @@ private: bool IsRejected(VAddr addr); /// Remembers the exception being handled as this address's rejection. void RememberRejection(VAddr addr); + /// Hash of the state task and mesh programs are translated with. + u64 MeshStateHash() const; Tegra::MaxwellDeviceMemoryManager& device_memory; diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index 9d189ad4f0..78438769f5 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -470,8 +470,16 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, gpu_memory->ReadBlock(program_base + start_address, &sph, sizeof(sph)); initial_offset = sizeof(sph); texture_bound = maxwell3d->regs.bindless_texture_const_buffer_slot; - if (maxwell3d->GetNativeMacroEngine().native_turing) { - if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel) { + const auto& regs{maxwell3d->regs}; + const bool native{maxwell3d->GetNativeMacroEngine().native_turing}; + // Task and mesh programs: the task in the vertex slot, its mesh program in the tessellation + // slot (without a task, the mesh program is in the vertex slot). + const bool mesh_slot{native && regs.IsMeshEnabled() && + (program == Maxwell::ShaderType::VertexB || + program == Maxwell::ShaderType::Tessellation)}; + if (native) { + if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel && + !mesh_slot) { throw Shader::RuntimeError("Unsupported native graphics shader stage {}", static_cast(program)); } if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { @@ -516,6 +524,19 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, ASSERT_MSG(false, "Invalid program={}", program); break; } + if (mesh_slot) { + const bool has_task{regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation)}; + stage = program == Maxwell::ShaderType::VertexB && has_task ? Shader::Stage::Task + : Shader::Stage::Mesh; + mesh_registers = {regs.reg_array[Maxwell::MeshShaderA], regs.reg_array[Maxwell::MeshShaderB], + regs.reg_array[Maxwell::MeshInitShader]}; + if (stage == Shader::Stage::Mesh && regs.IsShaderConfigEnabled(Maxwell::ShaderType::Geometry)) { + const auto& geometry{regs.pipelines[static_cast(Maxwell::ShaderType::Geometry)]}; + const GPUVAddr address{(static_cast(geometry.reserved[0] & 0xff) << 32) | + geometry.reserved[1]}; + gpu_memory->ReadBlock(address, &mesh_primitive_header, sizeof(mesh_primitive_header)); + } + } const u64 local_size{sph.LocalMemorySize()}; ASSERT(local_size <= (std::numeric_limits::max)()); local_memory_size = static_cast(local_size) + sph.common3.shader_local_memory_crs_size; diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 623ab7c30d..29b0c816fb 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1511,6 +1511,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, diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index d8524f6575..0d256c1dd9 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -65,6 +65,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) \ @@ -610,6 +611,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; diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index dbaaf450a0..ac67b95707 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); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 3010368973..f9b3cc15df 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{}; @@ -1319,6 +1320,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, -- cgit v1.2.3 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 ++++++--- src/video_core/vulkan_common/vulkan_memory_allocator.cpp | 5 ++++- 2 files changed, 10 insertions(+), 4 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, diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 1798611e26..273e8414ce 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(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(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, -- 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 From 51d798ff0373a40098aa77ef13d54307f39e44ac Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:38:05 +0100 Subject: texture_cache: keep the allocator's block slack out of the usable VRAM The collector's usage leaves out the unused part of VMA's blocks, but the driver counts it and a heap at its budget cannot give it to a new image. On an RX 7800 XT that slack was 1.06 GiB, the whole 1 GiB margin: the heap sat at its budget for a minute while the collector saw room, then images stopped fitting and the process died. Reserve the slack (capped at 1.5 GiB) on top of the margin: at once when it grows, slowly when it shrinks, so eviction leaving partly used blocks doesn't chase the threshold down. Assisted-by: Claude Sonnet 5.5 (diagnosis and code) (cherry picked from commit 73fd5a489ea28226fa16a55f5008e622d631b54c) --- .../renderer_vulkan/vk_texture_cache.cpp | 4 ++++ src/video_core/renderer_vulkan/vk_texture_cache.h | 2 ++ src/video_core/texture_cache/texture_cache.h | 23 ++++++++++++++++++++-- src/video_core/texture_cache/texture_cache_base.h | 1 + src/video_core/vulkan_common/vulkan_device.cpp | 10 ++++++++++ src/video_core/vulkan_common/vulkan_device.h | 8 +++++++- 6 files changed, 45 insertions(+), 3 deletions(-) (limited to 'src/video_core/vulkan_common/vulkan_device.cpp') diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 09e7961ff9..cb0453c178 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -2082,6 +2082,10 @@ u64 TextureCacheRuntime::GetDeviceMemoryBudget() const { return device.GetDeviceMemoryBudget(); } +u64 TextureCacheRuntime::GetDeviceMemorySlack() const { + return device.GetDeviceMemorySlack(); +} + bool TextureCacheRuntime::CanDownloadMsaa(const VideoCommon::ImageInfo& info) const { const VkImageAspectFlags aspect_mask = ImageAspectMask(info.format); if ((aspect_mask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 5ac833e787..d5f8756486 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -74,6 +74,8 @@ public: u64 GetDeviceMemoryBudget() const; + u64 GetDeviceMemorySlack() const; + bool CanReportMemoryUsage() const; /// Images that did not fit the device memory budget so far (see MemoryAllocator). diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index cf042c1488..45bc35dee6 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -313,14 +313,33 @@ void TextureCache

::TickFrame() { if (budget > 2_GiB) { const u64 old_critical = critical_memory; const u64 margin = (std::min)(budget / 8, 1_GiB); - critical_memory = budget - (std::min)(margin + pressure_margin, budget / 2); + if constexpr (requires { runtime.GetDeviceMemorySlack(); }) { + // The usage counted here leaves out the unused part of the allocator's + // blocks, which the driver counts and a full heap cannot hand to a new + // image (RX 7800 XT: 1.06 GiB of it, the whole margin, with the heap at + // its budget for a minute before images stopped fitting). Reserve it + // too: at once when it grows, slowly when it shrinks, so evicting images + // (which leaves partly used blocks) does not chase the threshold down. + const u64 slack = (std::min)(runtime.GetDeviceMemorySlack(), 1536_MiB); + slack_margin = slack >= slack_margin + ? slack + : slack_margin - (std::min)(slack_margin - slack, 16_MiB); + } + critical_memory = + budget - (std::min)(margin + pressure_margin + slack_margin, budget / 2); expected_memory = critical_memory - (std::min)(margin, critical_memory / 2); // Log the first values and real changes (the budget moves by a few MiB all // the time). const u64 change = critical_memory > old_critical ? critical_memory - old_critical : old_critical - critical_memory; - ApplyThresholdOverrides(budget_check_frames == 1 || change >= 256_MiB); + const bool log = budget_check_frames == 1 || change >= 256_MiB; + ApplyThresholdOverrides(log); + if (log) { + LOG_INFO(HW_GPU, + "Memory margins: base {} MiB, out of memory {} MiB, block slack {} MiB", + margin >> 20, pressure_margin >> 20, slack_margin >> 20); + } } } } diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 1a6931d18a..24ebaf0689 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -139,6 +139,7 @@ class TextureCache : public VideoCommon::ChannelSetupCaches astc_scan_buffer; diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 2ecbd2cb94..01e13cb384 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1684,6 +1684,16 @@ u64 Device::GetDeviceMemoryUsage() const { return result; } +u64 Device::GetDeviceMemorySlack() const { + if (allocator == nullptr) { + return 0; + } + std::array 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 2d19321145..622d792210 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -1019,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; } -- cgit v1.2.3