From 04f3a93854bf2602f2fa18123d9e47cfc77ba708 Mon Sep 17 00:00:00 2001 From: Sergei Golishnikov Date: Wed, 17 Jun 2026 18:15:42 +0300 Subject: [PATCH] macos: fix Vulkan/Metal renderer on macOS 26+ and bump MoltenVK (#2149) * macos: fix Vulkan/Metal renderer on macOS 26+ and bump MoltenVK Four related changes that together let the Vulkan backend boot a game on Apple Silicon Macs running macOS 26.x. Each is needed independently; the combination was tested on a Mac16,6 (M4 Max) / macOS 26.4.1. CMakeModules/DownloadExternals.cmake: Bump bundled MoltenVK from v1.2.9 to v1.4.1. v1.2.9 does not recognize macOS 26 surface objects and traps with an Objective-C "unrecognized selector" inside vkGetPhysicalDeviceSurfaceCapabilitiesKHR. src/video_core/renderer_vulkan/vk_instance.cpp: Disable VK_EXT_tooling_info on MoltenVK. MoltenVK advertises the extension but does not expose vkGetPhysicalDeviceToolPropertiesEXT through its dispatcher, so the call inside CollectToolingInfo() trips a vulkan-hpp assertion in Debug and dereferences NULL in Release. src/citra_qt/bootmanager.cpp: Force-install a fresh CAMetalLayer on the NSView before handing it to createMetalSurfaceEXT. Qt 6.11's QWindow::MetalSurface does not always back the view with a CAMetalLayer on macOS 26, and MoltenVK 1.3+ aborts in MVKSurface::getNaturalExtent() if the layer is not the right class. Implemented via objc_msgSend so no .mm conversion is needed. src/citra_meta/CMakeLists.txt: Gate LaunchScreen.storyboard behind 'if (IOS)'. The storyboard is iOS-only and previously broke the Xcode generator on macOS with "iOS storyboards do not support target device type 'mac'." * Move Metal layer creation logic into MetalUtil::CreateMetalLayer ObjC++ function This change makes the code much more readable, as the logic is now in its native language --------- Co-authored-by: SergeyMild <> Co-authored-by: OpenSauce04 --- CMakeModules/DownloadExternals.cmake | 2 +- src/citra_meta/CMakeLists.txt | 4 +++- src/citra_qt/CMakeLists.txt | 2 ++ src/citra_qt/bootmanager.cpp | 6 ++--- src/citra_qt/util/metal_util.h | 11 +++++++++ src/citra_qt/util/metal_util.mm | 23 +++++++++++++++++++ .../renderer_vulkan/vk_instance.cpp | 3 ++- 7 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 src/citra_qt/util/metal_util.h create mode 100644 src/citra_qt/util/metal_util.mm diff --git a/CMakeModules/DownloadExternals.cmake b/CMakeModules/DownloadExternals.cmake index 12178a49f..3cbf4511d 100644 --- a/CMakeModules/DownloadExternals.cmake +++ b/CMakeModules/DownloadExternals.cmake @@ -174,7 +174,7 @@ function(download_moltenvk) set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar") if (NOT EXISTS "${CMAKE_BINARY_DIR}/externals/MoltenVK") if (NOT EXISTS ${MOLTENVK_TAR}) - file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.9/MoltenVK-all.tar + file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.1/MoltenVK-all.tar ${MOLTENVK_TAR} SHOW_PROGRESS) endif() diff --git a/src/citra_meta/CMakeLists.txt b/src/citra_meta/CMakeLists.txt index 156799b03..5d477bf87 100644 --- a/src/citra_meta/CMakeLists.txt +++ b/src/citra_meta/CMakeLists.txt @@ -15,9 +15,11 @@ if (APPLE) set(DIST_DIR "../../dist/apple") set(APPLE_RESOURCES "${DIST_DIR}/azahar.icns" - "${DIST_DIR}/LaunchScreen.storyboard" "${DIST_DIR}/launch_logo.png" ) + if (IOS) + list(APPEND APPLE_RESOURCES "${DIST_DIR}/LaunchScreen.storyboard") + endif() target_sources(citra_meta PRIVATE ${APPLE_RESOURCES}) # Define app bundle metadata. diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 33137ac98..f870f770c 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -202,6 +202,8 @@ if (APPLE) target_sources(citra_qt PUBLIC qt_swizzle.h qt_swizzle.mm + util/metal_util.h + util/metal_util.mm ) endif() diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index c0d6f613f..7ef7af7aa 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -37,8 +37,7 @@ #endif #if defined(__APPLE__) -#include -#include +#include "util/metal_util.h" #elif !defined(WIN32) #include #endif @@ -420,8 +419,7 @@ static Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow* window // Our Win32 Qt external doesn't have the private API. wsi.render_surface = reinterpret_cast(window->winId()); #elif defined(__APPLE__) - wsi.render_surface = reinterpret_cast(objc_msgSend)( - reinterpret_cast(window->winId()), sel_registerName("layer")); + wsi.render_surface = MetalUtil::CreateMetalLayer(window->winId()); #else QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface(); wsi.display_connection = pni->nativeResourceForWindow("display", window); diff --git a/src/citra_qt/util/metal_util.h b/src/citra_qt/util/metal_util.h new file mode 100644 index 000000000..8f0c85ba2 --- /dev/null +++ b/src/citra_qt/util/metal_util.h @@ -0,0 +1,11 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#import "qwindowdefs.h" + +namespace MetalUtil { + +void* CreateMetalLayer(WId window); + +} diff --git a/src/citra_qt/util/metal_util.mm b/src/citra_qt/util/metal_util.mm new file mode 100644 index 000000000..6680f1df6 --- /dev/null +++ b/src/citra_qt/util/metal_util.mm @@ -0,0 +1,23 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#import +#import +#import + +namespace MetalUtil { + +void* CreateMetalLayer(WId winId) { + // Qt's MetalSurface QWindow on macOS does not always back the NSView + // with a CAMetalLayer immediately; MoltenVK 1.3+ aborts in + // MVKSurface::getNaturalExtent() if the layer is not CAMetalLayer. + // Force-install a fresh CAMetalLayer on the view. + CAMetalLayer* metalLayer = [CAMetalLayer layer]; + NSView* view = (__bridge NSView*)winId; + view.wantsLayer = YES; + view.layer = metalLayer; + return (__bridge void*)metalLayer; +} + +} // namespace MetalUtil diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index d86cc0f67..752f0e316 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -451,7 +451,8 @@ bool Instance::CreateDevice() { image_format_list = add_extension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); shader_stencil_export = add_extension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); external_memory_host = add_extension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); - tooling_info = add_extension(VK_EXT_TOOLING_INFO_EXTENSION_NAME); + tooling_info = add_extension(VK_EXT_TOOLING_INFO_EXTENSION_NAME, is_moltenvk, + "function pointer is not exposed by MoltenVK"); const bool has_timeline_semaphores = add_extension(VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, is_qualcomm || is_turnip, "it is broken on Qualcomm drivers");