From 9ff1e20103a885f688b80e0bff289a50941872ac Mon Sep 17 00:00:00 2001 From: shpontex Date: Sun, 8 Nov 2026 15:38:51 +0100 Subject: [PATCH] vulkan: clamp framebuffers to device limits on macOS OoT leaves attachment-less draws at 32768x32768; Apple GPUs max out at 16384 and Metal loses the device after the intro. Also pad the stream buffer by 64 KiB so Metal cbuf reads near the end stay in bounds. Assisted-by: Cursor (ported Lichii#427 / diagnosed crash) --- src/tests/CMakeLists.txt | 1 + src/tests/video_core/framebuffer_limits.cpp | 39 +++++++++++++++++++ src/video_core/CMakeLists.txt | 1 + .../renderer_vulkan/framebuffer_limits.h | 27 +++++++++++++ .../vk_staging_buffer_pool.cpp | 8 +++- .../renderer_vulkan/vk_texture_cache.cpp | 8 ++++ src/video_core/vulkan_common/vulkan_device.h | 5 ++- 7 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/tests/video_core/framebuffer_limits.cpp create mode 100644 src/video_core/renderer_vulkan/framebuffer_limits.h diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index d38658d6a9..e7816e7d06 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(tests core/priority_queue.cpp core/internal_network/network.cpp video_core/frame_dump_schedule.cpp + video_core/framebuffer_limits.cpp video_core/memory_tracker.cpp video_core/spirv_storage_alignment.cpp video_core/pipeline_optimization.cpp diff --git a/src/tests/video_core/framebuffer_limits.cpp b/src/tests/video_core/framebuffer_limits.cpp new file mode 100644 index 0000000000..e92c9fddb7 --- /dev/null +++ b/src/tests/video_core/framebuffer_limits.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/renderer_vulkan/framebuffer_limits.h" + +namespace { +constexpr Vulkan::FramebufferSize APPLE_LIMITS{16384, 16384, 2048}; +} // Anonymous namespace + +TEST_CASE("Attachment-less framebuffers stay within the device limits", + "[video_core][framebuffer_limits]") { + const auto size = Vulkan::ClampFramebufferSize({32768, 32768, 1}, APPLE_LIMITS); + REQUIRE(size.width == 16384); + REQUIRE(size.height == 16384); + REQUIRE(size.layers == 1); +} + +TEST_CASE("Framebuffer sizes within the device limits are unchanged", + "[video_core][framebuffer_limits]") { + const auto size = Vulkan::ClampFramebufferSize({1920, 1080, 6}, APPLE_LIMITS); + REQUIRE(size.width == 1920); + REQUIRE(size.height == 1080); + REQUIRE(size.layers == 6); +} + +TEST_CASE("Framebuffer layers are clamped to the device limit", + "[video_core][framebuffer_limits]") { + const auto size = Vulkan::ClampFramebufferSize({64, 64, 4096}, APPLE_LIMITS); + REQUIRE(size.layers == 2048); +} + +TEST_CASE("Framebuffers keep at least one pixel and one layer", + "[video_core][framebuffer_limits]") { + const auto size = Vulkan::ClampFramebufferSize({0, 0, 0}, APPLE_LIMITS); + REQUIRE(size.width == 1); + REQUIRE(size.height == 1); + REQUIRE(size.layers == 1); +} diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index c0648802ee..136dcc6f2e 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -159,6 +159,7 @@ add_library(video_core STATIC renderer_vulkan/blit_image.h renderer_vulkan/fixed_pipeline_state.cpp renderer_vulkan/fixed_pipeline_state.h + renderer_vulkan/framebuffer_limits.h renderer_vulkan/maxwell_to_vk.cpp renderer_vulkan/maxwell_to_vk.h renderer_vulkan/pipeline_helper.h diff --git a/src/video_core/renderer_vulkan/framebuffer_limits.h b/src/video_core/renderer_vulkan/framebuffer_limits.h new file mode 100644 index 0000000000..d11cf19868 --- /dev/null +++ b/src/video_core/renderer_vulkan/framebuffer_limits.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace Vulkan { + +struct FramebufferSize { + u32 width; + u32 height; + u32 layers; +}; + +// Draws without attachments take their size from the surface clip, which games leave at +// 32768x32768. Apple GPUs allow 16384, and Metal loses the device on a larger framebuffer. +constexpr FramebufferSize ClampFramebufferSize(FramebufferSize size, FramebufferSize max) { + return { + (std::max)(1U, (std::min)(size.width, max.width)), + (std::max)(1U, (std::min)(size.height, max.height)), + (std::max)(1U, (std::min)(size.layers, max.layers)), + }; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp index 98787b9add..f2bb0ee10b 100644 --- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp @@ -40,6 +40,12 @@ constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB; constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB; #endif +// Unused space after the end of the stream buffer. Constant buffers are bound from it and the +// shaders declare each one at its full 64 KiB, so Metal checks 64 KiB from the binding offset +// against the end of the buffer ("argument c3[0] ... has space for 26624 bytes, but argument has a +// length(65536)"). The tail is never handed out, so every binding has the full 64 KiB behind it. +constexpr VkDeviceSize STREAM_BUFFER_TAIL = 64_KiB; + size_t GetStreamBufferSize(const Device& device) { if (!device.HasDebuggingToolAttached()) { if (device.IsIntegrated()) { @@ -88,7 +94,7 @@ StagingBufferPool::StagingBufferPool(const Device& device_, MemoryAllocator& mem .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = nullptr, .flags = 0, - .size = stream_buffer_size, + .size = stream_buffer_size + STREAM_BUFFER_TAIL, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 97126571ab..d2e9780273 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -28,6 +28,7 @@ #include "video_core/engines/fermi_2d.h" #include "video_core/renderer_vulkan/blit_image.h" +#include "video_core/renderer_vulkan/framebuffer_limits.h" #include "video_core/renderer_vulkan/maxwell_to_vk.h" #include "video_core/renderer_vulkan/vk_compute_pass.h" #include "video_core/renderer_vulkan/vk_render_pass_cache.h" @@ -4501,6 +4502,13 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime, runtime_ptr = &runtime; render_area.width = (std::min)(render_area.width, width); render_area.height = (std::min)(render_area.height, height); + const FramebufferSize size = ClampFramebufferSize( + {render_area.width, render_area.height, + static_cast((std::max)(num_layers, 1))}, + {runtime.device.GetMaxFramebufferWidth(), runtime.device.GetMaxFramebufferHeight(), + runtime.device.GetMaxFramebufferLayers()}); + render_area = VkExtent2D{.width = size.width, .height = size.height}; + num_layers = static_cast(size.layers); if (do_resolve_color) { const u32 layers = static_cast((std::max)(num_layers, 1)); diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 9cbe7e0339..ff9cb18ba0 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -420,7 +420,10 @@ class Device { FN_MAX_LIMIT_ELEM(DescriptorSetStorageBuffersDynamic) \ FN_MAX_LIMIT_ELEM(DescriptorSetSampledImages) \ FN_MAX_LIMIT_ELEM(DescriptorSetStorageImages) \ - FN_MAX_LIMIT_ELEM(DescriptorSetInputAttachments) + FN_MAX_LIMIT_ELEM(DescriptorSetInputAttachments) \ + FN_MAX_LIMIT_ELEM(FramebufferWidth) \ + FN_MAX_LIMIT_ELEM(FramebufferHeight) \ + FN_MAX_LIMIT_ELEM(FramebufferLayers) #define FN_MAX_LIMIT_ELEM(name) \ u32 GetMax##name() const { return properties.properties.limits.max##name; } FN_MAX_LIMIT_LIST