From 471033b10aec3fa8e3506ec4942fd9c493035701 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 20 May 2021 12:27:36 -0700 Subject: [PATCH 1/7] Force CAMetalLayer to disable vsync. I have zero clue at the moment why this works. MoltenVK sets this property during initialization of the swapchain, and logging confirms it's set, but setting the proprty ahead of time seems to actually disable vsync and result in the expected rendering smoothness. At worst, this change does nothing that wouldn't be done by MoltenVK anyway. At best, it seems to provide much smoother render on a number of tested macOS setups (Intel 2015 MBP/Mojave, Intel 2015 MBP/Catalina, 2020 M1 MBP/Big Sur). --- .../Core/VideoBackends/Vulkan/SwapChain.cpp | 6 ++++++ Source/Core/VideoBackends/Vulkan/main.cpp | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Source/Core/VideoBackends/Vulkan/SwapChain.cpp b/Source/Core/VideoBackends/Vulkan/SwapChain.cpp index 8dd53b0546..5daa682e17 100644 --- a/Source/Core/VideoBackends/Vulkan/SwapChain.cpp +++ b/Source/Core/VideoBackends/Vulkan/SwapChain.cpp @@ -369,6 +369,12 @@ bool SwapChain::CreateSwapChain() res = vkCreateSwapchainKHR(g_vulkan_context->GetDevice(), &swap_chain_info, nullptr, &m_swap_chain); + + //MVKSurface *mvkSrfc = (MVKSurface *)swap_chain_info->surface; + //id layer = reinterpret_cast(objc_msgSend)((id)m_native_handle, sel_getUid("layer")); + //BOOL x = reinterpret_cast(objc_msgSend)(layer, sel_getUid("displaySyncEnabled")); + //ERROR_LOG(VIDEO, "Vulkan vsync status: %i", x); + if (res != VK_SUCCESS) { LOG_VULKAN_ERROR(res, "vkCreateSwapchainKHR failed: "); diff --git a/Source/Core/VideoBackends/Vulkan/main.cpp b/Source/Core/VideoBackends/Vulkan/main.cpp index 5609c4a880..f2c3c556df 100644 --- a/Source/Core/VideoBackends/Vulkan/main.cpp +++ b/Source/Core/VideoBackends/Vulkan/main.cpp @@ -322,9 +322,9 @@ void VideoBackend::PrepareWindow(void* window_handle) { ERROR_LOG(VIDEO, "Failed to create Metal layer."); return; } - + // [view setWantsLayer:YES] - reinterpret_cast(objc_msgSend)(view, sel_getUid("setWantsLayer:"), YES); + reinterpret_cast(objc_msgSend)(view, sel_getUid("setWantsLayer:"), YES); // [view setLayer:layer] reinterpret_cast(objc_msgSend)(view, sel_getUid("setLayer:"), layer); @@ -340,6 +340,21 @@ void VideoBackend::PrepareWindow(void* window_handle) { // layer.contentsScale = factor reinterpret_cast(objc_msgSend)(layer, sel_getUid("setContentsScale:"), factor); + + // This is an oddity, but alright. The SwapChain is already configured to be respective of Vsync, but the underlying + // CAMetalLayer *also* needs to be instructed to respect it. This defaults to YES; if we're not supposed to have vsync + // enabled, then we need to flip this. + // + // Notably, some M1 Macs have issues without this logic. + // + // I have absolutely no clue why this works, as MoltenVK also sets this property. Setting it before giving the layer + // to MoltenVK seems to make it stick, though. + if (!g_Config.IsVSync()) + { + // Explicitly tells the underlying layer to NOT use vsync. + // [view setDisplaySyncEnabled:NO] + reinterpret_cast(objc_msgSend)(layer, sel_getUid("setDisplaySyncEnabled:"), NO); + } #endif } } From e35b38ddbbc326c60c49d58c90ee4a85c82990da Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 18 Jun 2021 15:29:12 -0700 Subject: [PATCH 2/7] Inject a custom CALayer rendering target to avoid wxWidgets drawRect --- Source/Core/VideoBackends/Vulkan/Renderer.cpp | 13 +- Source/Core/VideoBackends/Vulkan/Renderer.h | 1 + Source/Core/VideoBackends/Vulkan/main.cpp | 147 ++++++++++++++---- Source/Core/VideoCommon/RenderBase.h | 4 +- 4 files changed, 136 insertions(+), 29 deletions(-) diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.cpp b/Source/Core/VideoBackends/Vulkan/Renderer.cpp index c7fa13800e..176313e234 100644 --- a/Source/Core/VideoBackends/Vulkan/Renderer.cpp +++ b/Source/Core/VideoBackends/Vulkan/Renderer.cpp @@ -1711,10 +1711,21 @@ void Renderer::SetViewport() StateTracker::GetInstance()->SetViewport(viewport); } +void Renderer::CacheSurfaceHandle(void* new_surface_handle) +{ + m_cached_surface_handle = new_surface_handle; +} + void Renderer::ChangeSurface(void* new_surface_handle) { // Called by the main thread when the window is resized. - m_new_surface_handle = new_surface_handle; +#if defined(__APPLE__) + m_new_surface_handle = m_cached_surface_handle; + // Might need to resize actual view? +#else + m_new_surface_handle = new_surface_handle; +#endif + m_surface_needs_change.Set(); m_surface_changed.Set(); } diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.h b/Source/Core/VideoBackends/Vulkan/Renderer.h index acc005aada..36a75ec10e 100644 --- a/Source/Core/VideoBackends/Vulkan/Renderer.h +++ b/Source/Core/VideoBackends/Vulkan/Renderer.h @@ -67,6 +67,7 @@ class Renderer : public ::Renderer void SetViewport() override; void ChangeSurface(void* new_surface_handle) override; + void CacheSurfaceHandle(void* new_surface_handle) override; private: bool CreateSemaphores(); diff --git a/Source/Core/VideoBackends/Vulkan/main.cpp b/Source/Core/VideoBackends/Vulkan/main.cpp index f2c3c556df..9e61304403 100644 --- a/Source/Core/VideoBackends/Vulkan/main.cpp +++ b/Source/Core/VideoBackends/Vulkan/main.cpp @@ -27,10 +27,15 @@ #if defined(VK_USE_PLATFORM_METAL_EXT) #include +#include +#include #endif namespace Vulkan { + +static void* s_metal_view_handle = nullptr; + void VideoBackend::InitBackendInfo() { VulkanContext::PopulateBackendInfo(&g_Config); @@ -117,8 +122,16 @@ bool VideoBackend::Initialize(void* window_handle) enable_validation_layer = false; } + // On macOS, we want to get the subview that hosts the rendering layer. Other platforms + // render through to the underlying view with no issues. +#if defined(VK_USE_PLATFORM_METAL_EXT) + void* win_handle = s_metal_view_handle; +#else + void* win_handle = window_handle; +#endif + // Create Vulkan instance, needed before we can create a surface. - bool enable_surface = window_handle != nullptr; + bool enable_surface = win_handle != nullptr; bool enable_debug_reports = ShouldEnableDebugReports(enable_validation_layer); VkInstance instance = VulkanContext::CreateVulkanInstance(enable_surface, enable_debug_reports, enable_validation_layer); @@ -144,7 +157,7 @@ bool VideoBackend::Initialize(void* window_handle) VkSurfaceKHR surface = VK_NULL_HANDLE; if (enable_surface) { - surface = SwapChain::CreateVulkanSurface(instance, window_handle); + surface = SwapChain::CreateVulkanSurface(instance, win_handle); if (surface == VK_NULL_HANDLE) { PanicAlert("Failed to create Vulkan surface."); @@ -192,7 +205,7 @@ bool VideoBackend::Initialize(void* window_handle) std::unique_ptr swap_chain; if (surface != VK_NULL_HANDLE) { - swap_chain = SwapChain::Create(window_handle, surface, g_Config.IsVSync()); + swap_chain = SwapChain::Create(win_handle, surface, g_Config.IsVSync()); if (!swap_chain) { PanicAlert("Failed to create Vulkan swap chain."); @@ -217,6 +230,13 @@ bool VideoBackend::Initialize(void* window_handle) g_framebuffer_manager = std::make_unique(); g_renderer = std::make_unique(std::move(swap_chain)); g_renderer->Init(); + + // We cache this on the renderer if it's Metal, as fullscreen changes need to use the + // correct rendering layer to handle swap chain recreation. +#if defined(VK_USE_PLATFORM_METAL_EXT) + g_renderer->CacheSurfaceHandle(s_metal_view_handle); +#endif + // Invoke init methods on main wrapper classes. // These have to be done before the others because the destructors // for the remaining classes may call methods on these. @@ -282,6 +302,10 @@ void VideoBackend::Shutdown() UnloadVulkanLibrary(); ShutdownShared(); + +#if defined(VK_USE_PLATFORM_METAL_EXT) + s_metal_view_handle = nullptr; +#endif } void VideoBackend::Video_Cleanup() @@ -301,41 +325,39 @@ void VideoBackend::Video_Cleanup() CleanupShared(); } -void VideoBackend::PrepareWindow(void* window_handle) { #if defined(VK_USE_PLATFORM_METAL_EXT) - id view = reinterpret_cast(window_handle); +// This is injected as a subclass method on the custom layer view, and +// tells macOS to avoid `drawRect:` and opt for direct layer updating instead. +BOOL wantsUpdateLayer(id self, SEL _cmd, id sender) +{ + return YES; +} - // This is kinda messy, but it avoids having to write Objective C++ just to create a metal layer. - //id view = reinterpret_cast(wsi.render_surface); - Class clsCAMetalLayer = objc_getClass("CAMetalLayer"); - if (!clsCAMetalLayer) - { - ERROR_LOG(VIDEO, "Failed to get CAMetalLayer class."); - return; - } +// Used by some internals, but ideally never gets called to begin with. +Class getLayerClass(id self, SEL _cmd) +{ + Class clsCAMetalLayerClass = objc_getClass("CAMetalLayer"); + return clsCAMetalLayerClass; +} + +// When `wantsLayer` is true, this method is invoked to create the actual backing layer. +id makeBackingLayer(id self, SEL _cmd) +{ + Class metalLayerClass = objc_getClass("CAMetalLayer"); - // [CAMetalLayer layer] - id layer = reinterpret_cast(objc_msgSend)(objc_getClass("CAMetalLayer"), - sel_getUid("layer")); - if (!layer) + // This should only be possible prior to macOS 10.14, but worth logging regardless. + if (!metalLayerClass) { - ERROR_LOG(VIDEO, "Failed to create Metal layer."); - return; + ERROR_LOG(VIDEO, "Failed to get CAMetalLayer class."); } - - // [view setWantsLayer:YES] - reinterpret_cast(objc_msgSend)(view, sel_getUid("setWantsLayer:"), YES); - // [view setLayer:layer] - reinterpret_cast(objc_msgSend)(view, sel_getUid("setLayer:"), layer); + id layer = reinterpret_cast(objc_msgSend)(metalLayerClass, sel_getUid("layer")); - // NSScreen* screen = [NSScreen mainScreen] id screen = reinterpret_cast(objc_msgSend)(objc_getClass("NSScreen"), sel_getUid("mainScreen")); // CGFloat factor = [screen backingScaleFactor] - double factor = - reinterpret_cast(objc_msgSend)(screen, sel_getUid("backingScaleFactor")); + double factor = reinterpret_cast(objc_msgSend)(screen, sel_getUid("backingScaleFactor")); // layer.contentsScale = factor reinterpret_cast(objc_msgSend)(layer, sel_getUid("setContentsScale:"), @@ -354,7 +376,78 @@ void VideoBackend::PrepareWindow(void* window_handle) { // Explicitly tells the underlying layer to NOT use vsync. // [view setDisplaySyncEnabled:NO] reinterpret_cast(objc_msgSend)(layer, sel_getUid("setDisplaySyncEnabled:"), NO); + + // CAMetalLayer is triple-buffered by default; we can lower this to double buffering. + // + // (The only acceptable values are `2` or `3`). + reinterpret_cast(objc_msgSend)(layer, sel_getUid("setMaximumDrawableCount:"), 2); + } + + return layer; +} + +constexpr char kSLPMetalLayerViewClassName[] = "SLPMetalLayerViewClass"; + +// This method injects a custom NSView subclass into the Objective-C runtime. +// +// The reason this is done is due to wanting to bypass NSView's `drawRect:` for Metal rendering +// purposes. To do this, it's not enough to just set `wantsLayer` to true - we need to also implement +// a few subclass methods, and tell the system we *want* the fast path. +// +// We have to inject a custom subclass as we can't modify the view (window_handle) in `PrepareWindow`, +// as that's a wxWidgets handle that relies on `drawRect:` being called for things to work. To work +// around this, we simply take the `window_handle` (i.e the view), create an instance of our `SLPMetalLayerView`, +// and attach that as a child view. `SLPMetalLayerView` should get the fast path, while everything else should +// stay golden. +Class getSLPMetalLayerViewClassType() +{ + Class SLPMetalLayerViewClass = objc_getClass(kSLPMetalLayerViewClassName); + + if (SLPMetalLayerViewClass == nullptr) + { + // This does a one-time opt-in to a MVK flag that seems to universally help in Ishiiruka. + // (mainline should not need this) + setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0); + setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); + + SLPMetalLayerViewClass = objc_allocateClassPair( + (Class)objc_getClass("NSView"), + kSLPMetalLayerViewClassName, + 0 + ); + + class_addMethod(SLPMetalLayerViewClass, sel_getUid("layerClass"), (IMP)getLayerClass, "@:@"); + class_addMethod(SLPMetalLayerViewClass, sel_getUid("wantsUpdateLayer"), (IMP)wantsUpdateLayer, "v@:"); + class_addMethod(SLPMetalLayerViewClass, sel_getUid("makeBackingLayer"), (IMP)makeBackingLayer, "@:@"); + class_addMethod(SLPMetalLayerViewClass, sel_getUid("isOpaque"), (IMP)wantsUpdateLayer, "v@:"); + objc_registerClassPair(SLPMetalLayerViewClass); } + + return SLPMetalLayerViewClass; +} +#endif + +void VideoBackend::PrepareWindow(void* window_handle) { +#if defined(VK_USE_PLATFORM_METAL_EXT) + id view = reinterpret_cast(window_handle); + + CGRect (*sendRectFn)(id receiver, SEL operation); + sendRectFn = (CGRect(*)(id, SEL))objc_msgSend_stret; + CGRect frame = sendRectFn(view, sel_getUid("frame")); + + Class SLPMetalLayerViewClass = getSLPMetalLayerViewClassType(); + id alloc = reinterpret_cast(objc_msgSend)(SLPMetalLayerViewClass, sel_getUid("alloc")); + + //auto rect = (CGRect){{0, 0}, {CGFloat(width), CGFloat(height)}}; + auto rect = (CGRect){{0, 0}, {frame.size.width, frame.size.height}}; + id metal_view = reinterpret_cast(objc_msgSend)(alloc, sel_getUid("initWithFrame:"), rect); + objc_msgSend(metal_view, sel_getUid("setWantsLayer:"), YES); + + // The below does: objc_msgSend(view, sel_getUid("setAutoresizingMask"), NSViewWidthSizable | NSViewHeightSizable); + objc_msgSend(metal_view, sel_getUid("setAutoresizingMask:"), 18); + + objc_msgSend(view, sel_getUid("addSubview:"), metal_view); + s_metal_view_handle = metal_view; #endif } } diff --git a/Source/Core/VideoCommon/RenderBase.h b/Source/Core/VideoCommon/RenderBase.h index 427b4ad57a..45a2900afb 100644 --- a/Source/Core/VideoCommon/RenderBase.h +++ b/Source/Core/VideoCommon/RenderBase.h @@ -149,6 +149,7 @@ class Renderer // Final surface changing // This is called when the surface is resized (WX) or the window changes (Android). virtual void ChangeSurface(void* new_surface_handle) {} + virtual void CacheSurfaceHandle(void* new_surface_handle) {} bool UseVertexDepthRange() const; protected: std::tuple CalculateTargetScale(int x, int y) const; @@ -188,6 +189,7 @@ class Renderer Common::Flag m_surface_needs_change; Common::Event m_surface_changed; void* m_new_surface_handle = nullptr; + void* m_cached_surface_handle = nullptr; private: void RunFrameDumps(); void ShutdownFrameDumping(); @@ -231,4 +233,4 @@ class Renderer }; -extern std::unique_ptr g_renderer; \ No newline at end of file +extern std::unique_ptr g_renderer; From dc25035ca02c8dc99d2c9ea55f0113bce5d58557 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 18 Jun 2021 17:02:43 -0700 Subject: [PATCH 3/7] ENV feature flag this --- Source/Core/VideoBackends/Vulkan/main.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Core/VideoBackends/Vulkan/main.cpp b/Source/Core/VideoBackends/Vulkan/main.cpp index 9e61304403..0f0230f2a6 100644 --- a/Source/Core/VideoBackends/Vulkan/main.cpp +++ b/Source/Core/VideoBackends/Vulkan/main.cpp @@ -375,11 +375,15 @@ id makeBackingLayer(id self, SEL _cmd) { // Explicitly tells the underlying layer to NOT use vsync. // [view setDisplaySyncEnabled:NO] - reinterpret_cast(objc_msgSend)(layer, sel_getUid("setDisplaySyncEnabled:"), NO); - - // CAMetalLayer is triple-buffered by default; we can lower this to double buffering. - // - // (The only acceptable values are `2` or `3`). + reinterpret_cast(objc_msgSend)(layer, sel_getUid("setDisplaySyncEnabled:"), NO); + } + + // CAMetalLayer is triple-buffered by default; we can lower this to double buffering. + // + // (The only acceptable values are `2` or `3`). Typically it's only iMacs that can handle this, so we'll just + // enable an ENV variable for it and document it on the wiki. + if (getenv("SLP_METAL_DOUBLE_BUFFER") != NULL) + { reinterpret_cast(objc_msgSend)(layer, sel_getUid("setMaximumDrawableCount:"), 2); } @@ -438,12 +442,12 @@ void VideoBackend::PrepareWindow(void* window_handle) { Class SLPMetalLayerViewClass = getSLPMetalLayerViewClassType(); id alloc = reinterpret_cast(objc_msgSend)(SLPMetalLayerViewClass, sel_getUid("alloc")); - //auto rect = (CGRect){{0, 0}, {CGFloat(width), CGFloat(height)}}; auto rect = (CGRect){{0, 0}, {frame.size.width, frame.size.height}}; id metal_view = reinterpret_cast(objc_msgSend)(alloc, sel_getUid("initWithFrame:"), rect); objc_msgSend(metal_view, sel_getUid("setWantsLayer:"), YES); // The below does: objc_msgSend(view, sel_getUid("setAutoresizingMask"), NSViewWidthSizable | NSViewHeightSizable); + // All this is doing is telling the view/layer to resize when the parent does. objc_msgSend(metal_view, sel_getUid("setAutoresizingMask:"), 18); objc_msgSend(view, sel_getUid("addSubview:"), metal_view); From ca7572c083392a0d38cc465d3c5e27b31efc7f26 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 21 Jun 2021 22:39:27 -0700 Subject: [PATCH 4/7] Formatting --- Source/Core/VideoBackends/Vulkan/Renderer.cpp | 7 +- Source/Core/VideoBackends/Vulkan/Renderer.h | 2 +- .../Core/VideoBackends/Vulkan/SwapChain.cpp | 5 - Source/Core/VideoBackends/Vulkan/main.cpp | 156 +++++++++--------- Source/Core/VideoCommon/RenderBase.h | 2 +- 5 files changed, 83 insertions(+), 89 deletions(-) diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.cpp b/Source/Core/VideoBackends/Vulkan/Renderer.cpp index 176313e234..d3b6b91849 100644 --- a/Source/Core/VideoBackends/Vulkan/Renderer.cpp +++ b/Source/Core/VideoBackends/Vulkan/Renderer.cpp @@ -1713,17 +1713,16 @@ void Renderer::SetViewport() void Renderer::CacheSurfaceHandle(void* new_surface_handle) { - m_cached_surface_handle = new_surface_handle; + m_cached_surface_handle = new_surface_handle; } void Renderer::ChangeSurface(void* new_surface_handle) { // Called by the main thread when the window is resized. #if defined(__APPLE__) - m_new_surface_handle = m_cached_surface_handle; - // Might need to resize actual view? + m_new_surface_handle = m_cached_surface_handle; #else - m_new_surface_handle = new_surface_handle; + m_new_surface_handle = new_surface_handle; #endif m_surface_needs_change.Set(); diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.h b/Source/Core/VideoBackends/Vulkan/Renderer.h index 36a75ec10e..3ad5c45ef4 100644 --- a/Source/Core/VideoBackends/Vulkan/Renderer.h +++ b/Source/Core/VideoBackends/Vulkan/Renderer.h @@ -67,7 +67,7 @@ class Renderer : public ::Renderer void SetViewport() override; void ChangeSurface(void* new_surface_handle) override; - void CacheSurfaceHandle(void* new_surface_handle) override; + void CacheSurfaceHandle(void* new_surface_handle) override; private: bool CreateSemaphores(); diff --git a/Source/Core/VideoBackends/Vulkan/SwapChain.cpp b/Source/Core/VideoBackends/Vulkan/SwapChain.cpp index 5daa682e17..74aff0c2d5 100644 --- a/Source/Core/VideoBackends/Vulkan/SwapChain.cpp +++ b/Source/Core/VideoBackends/Vulkan/SwapChain.cpp @@ -370,11 +370,6 @@ bool SwapChain::CreateSwapChain() res = vkCreateSwapchainKHR(g_vulkan_context->GetDevice(), &swap_chain_info, nullptr, &m_swap_chain); - //MVKSurface *mvkSrfc = (MVKSurface *)swap_chain_info->surface; - //id layer = reinterpret_cast(objc_msgSend)((id)m_native_handle, sel_getUid("layer")); - //BOOL x = reinterpret_cast(objc_msgSend)(layer, sel_getUid("displaySyncEnabled")); - //ERROR_LOG(VIDEO, "Vulkan vsync status: %i", x); - if (res != VK_SUCCESS) { LOG_VULKAN_ERROR(res, "vkCreateSwapchainKHR failed: "); diff --git a/Source/Core/VideoBackends/Vulkan/main.cpp b/Source/Core/VideoBackends/Vulkan/main.cpp index 0f0230f2a6..4fe03dfd33 100644 --- a/Source/Core/VideoBackends/Vulkan/main.cpp +++ b/Source/Core/VideoBackends/Vulkan/main.cpp @@ -122,12 +122,12 @@ bool VideoBackend::Initialize(void* window_handle) enable_validation_layer = false; } - // On macOS, we want to get the subview that hosts the rendering layer. Other platforms - // render through to the underlying view with no issues. + // On macOS, we want to get the subview that hosts the rendering layer. Other platforms + // render through to the underlying view with no issues. #if defined(VK_USE_PLATFORM_METAL_EXT) - void* win_handle = s_metal_view_handle; + void* win_handle = s_metal_view_handle; #else - void* win_handle = window_handle; + void* win_handle = window_handle; #endif // Create Vulkan instance, needed before we can create a surface. @@ -231,10 +231,10 @@ bool VideoBackend::Initialize(void* window_handle) g_renderer = std::make_unique(std::move(swap_chain)); g_renderer->Init(); - // We cache this on the renderer if it's Metal, as fullscreen changes need to use the - // correct rendering layer to handle swap chain recreation. + // We cache this on the renderer if it's Metal, as fullscreen changes need to use the + // correct rendering layer to handle swap chain recreation. #if defined(VK_USE_PLATFORM_METAL_EXT) - g_renderer->CacheSurfaceHandle(s_metal_view_handle); + g_renderer->CacheSurfaceHandle(s_metal_view_handle); #endif // Invoke init methods on main wrapper classes. @@ -304,7 +304,7 @@ void VideoBackend::Shutdown() ShutdownShared(); #if defined(VK_USE_PLATFORM_METAL_EXT) - s_metal_view_handle = nullptr; + s_metal_view_handle = nullptr; #endif } @@ -330,28 +330,28 @@ void VideoBackend::Video_Cleanup() // tells macOS to avoid `drawRect:` and opt for direct layer updating instead. BOOL wantsUpdateLayer(id self, SEL _cmd, id sender) { - return YES; + return YES; } // Used by some internals, but ideally never gets called to begin with. Class getLayerClass(id self, SEL _cmd) { - Class clsCAMetalLayerClass = objc_getClass("CAMetalLayer"); - return clsCAMetalLayerClass; + Class clsCAMetalLayerClass = objc_getClass("CAMetalLayer"); + return clsCAMetalLayerClass; } // When `wantsLayer` is true, this method is invoked to create the actual backing layer. id makeBackingLayer(id self, SEL _cmd) { - Class metalLayerClass = objc_getClass("CAMetalLayer"); + Class metalLayerClass = objc_getClass("CAMetalLayer"); - // This should only be possible prior to macOS 10.14, but worth logging regardless. + // This should only be possible prior to macOS 10.14, but worth logging regardless. if (!metalLayerClass) { - ERROR_LOG(VIDEO, "Failed to get CAMetalLayer class."); + ERROR_LOG(VIDEO, "Failed to get CAMetalLayer class."); } - id layer = reinterpret_cast(objc_msgSend)(metalLayerClass, sel_getUid("layer")); + id layer = reinterpret_cast(objc_msgSend)(metalLayerClass, sel_getUid("layer")); id screen = reinterpret_cast(objc_msgSend)(objc_getClass("NSScreen"), sel_getUid("mainScreen")); @@ -363,31 +363,31 @@ id makeBackingLayer(id self, SEL _cmd) reinterpret_cast(objc_msgSend)(layer, sel_getUid("setContentsScale:"), factor); - // This is an oddity, but alright. The SwapChain is already configured to be respective of Vsync, but the underlying - // CAMetalLayer *also* needs to be instructed to respect it. This defaults to YES; if we're not supposed to have vsync - // enabled, then we need to flip this. - // - // Notably, some M1 Macs have issues without this logic. - // - // I have absolutely no clue why this works, as MoltenVK also sets this property. Setting it before giving the layer - // to MoltenVK seems to make it stick, though. - if (!g_Config.IsVSync()) - { - // Explicitly tells the underlying layer to NOT use vsync. - // [view setDisplaySyncEnabled:NO] - reinterpret_cast(objc_msgSend)(layer, sel_getUid("setDisplaySyncEnabled:"), NO); - } + // This is an oddity, but alright. The SwapChain is already configured to be respective of Vsync, but the underlying + // CAMetalLayer *also* needs to be instructed to respect it. This defaults to YES; if we're not supposed to have vsync + // enabled, then we need to flip this. + // + // Notably, some M1 Macs have issues without this logic. + // + // I have absolutely no clue why this works, as MoltenVK also sets this property. Setting it before giving the layer + // to MoltenVK seems to make it stick, though. + if (!g_Config.IsVSync()) + { + // Explicitly tells the underlying layer to NOT use vsync. + // [view setDisplaySyncEnabled:NO] + reinterpret_cast(objc_msgSend)(layer, sel_getUid("setDisplaySyncEnabled:"), NO); + } - // CAMetalLayer is triple-buffered by default; we can lower this to double buffering. - // - // (The only acceptable values are `2` or `3`). Typically it's only iMacs that can handle this, so we'll just - // enable an ENV variable for it and document it on the wiki. - if (getenv("SLP_METAL_DOUBLE_BUFFER") != NULL) - { - reinterpret_cast(objc_msgSend)(layer, sel_getUid("setMaximumDrawableCount:"), 2); - } - - return layer; + // CAMetalLayer is triple-buffered by default; we can lower this to double buffering. + // + // (The only acceptable values are `2` or `3`). Typically it's only iMacs that can handle this, so we'll just + // enable an ENV variable for it and document it on the wiki. + if (getenv("SLP_METAL_DOUBLE_BUFFER") != NULL) + { + reinterpret_cast(objc_msgSend)(layer, sel_getUid("setMaximumDrawableCount:"), 2); + } + + return layer; } constexpr char kSLPMetalLayerViewClassName[] = "SLPMetalLayerViewClass"; @@ -405,29 +405,29 @@ constexpr char kSLPMetalLayerViewClassName[] = "SLPMetalLayerViewClass"; // stay golden. Class getSLPMetalLayerViewClassType() { - Class SLPMetalLayerViewClass = objc_getClass(kSLPMetalLayerViewClassName); - - if (SLPMetalLayerViewClass == nullptr) - { - // This does a one-time opt-in to a MVK flag that seems to universally help in Ishiiruka. - // (mainline should not need this) - setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0); - setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); - - SLPMetalLayerViewClass = objc_allocateClassPair( - (Class)objc_getClass("NSView"), - kSLPMetalLayerViewClassName, - 0 - ); - - class_addMethod(SLPMetalLayerViewClass, sel_getUid("layerClass"), (IMP)getLayerClass, "@:@"); - class_addMethod(SLPMetalLayerViewClass, sel_getUid("wantsUpdateLayer"), (IMP)wantsUpdateLayer, "v@:"); - class_addMethod(SLPMetalLayerViewClass, sel_getUid("makeBackingLayer"), (IMP)makeBackingLayer, "@:@"); - class_addMethod(SLPMetalLayerViewClass, sel_getUid("isOpaque"), (IMP)wantsUpdateLayer, "v@:"); - objc_registerClassPair(SLPMetalLayerViewClass); - } - - return SLPMetalLayerViewClass; + Class SLPMetalLayerViewClass = objc_getClass(kSLPMetalLayerViewClassName); + + if (SLPMetalLayerViewClass == nullptr) + { + // This does a one-time opt-in to a MVK flag that seems to universally help in Ishiiruka. + // (mainline should not need this) + setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0); + setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); + + SLPMetalLayerViewClass = objc_allocateClassPair( + (Class)objc_getClass("NSView"), + kSLPMetalLayerViewClassName, + 0 + ); + + class_addMethod(SLPMetalLayerViewClass, sel_getUid("layerClass"), (IMP)getLayerClass, "@:@"); + class_addMethod(SLPMetalLayerViewClass, sel_getUid("wantsUpdateLayer"), (IMP)wantsUpdateLayer, "v@:"); + class_addMethod(SLPMetalLayerViewClass, sel_getUid("makeBackingLayer"), (IMP)makeBackingLayer, "@:@"); + class_addMethod(SLPMetalLayerViewClass, sel_getUid("isOpaque"), (IMP)wantsUpdateLayer, "v@:"); + objc_registerClassPair(SLPMetalLayerViewClass); + } + + return SLPMetalLayerViewClass; } #endif @@ -435,23 +435,23 @@ void VideoBackend::PrepareWindow(void* window_handle) { #if defined(VK_USE_PLATFORM_METAL_EXT) id view = reinterpret_cast(window_handle); - CGRect (*sendRectFn)(id receiver, SEL operation); - sendRectFn = (CGRect(*)(id, SEL))objc_msgSend_stret; - CGRect frame = sendRectFn(view, sel_getUid("frame")); - - Class SLPMetalLayerViewClass = getSLPMetalLayerViewClassType(); - id alloc = reinterpret_cast(objc_msgSend)(SLPMetalLayerViewClass, sel_getUid("alloc")); + CGRect (*sendRectFn)(id receiver, SEL operation); + sendRectFn = (CGRect(*)(id, SEL))objc_msgSend_stret; + CGRect frame = sendRectFn(view, sel_getUid("frame")); - auto rect = (CGRect){{0, 0}, {frame.size.width, frame.size.height}}; - id metal_view = reinterpret_cast(objc_msgSend)(alloc, sel_getUid("initWithFrame:"), rect); - objc_msgSend(metal_view, sel_getUid("setWantsLayer:"), YES); - - // The below does: objc_msgSend(view, sel_getUid("setAutoresizingMask"), NSViewWidthSizable | NSViewHeightSizable); - // All this is doing is telling the view/layer to resize when the parent does. - objc_msgSend(metal_view, sel_getUid("setAutoresizingMask:"), 18); - - objc_msgSend(view, sel_getUid("addSubview:"), metal_view); - s_metal_view_handle = metal_view; + Class SLPMetalLayerViewClass = getSLPMetalLayerViewClassType(); + id alloc = reinterpret_cast(objc_msgSend)(SLPMetalLayerViewClass, sel_getUid("alloc")); + + auto rect = (CGRect){{0, 0}, {frame.size.width, frame.size.height}}; + id metal_view = reinterpret_cast(objc_msgSend)(alloc, sel_getUid("initWithFrame:"), rect); + objc_msgSend(metal_view, sel_getUid("setWantsLayer:"), YES); + + // The below does: objc_msgSend(view, sel_getUid("setAutoresizingMask"), NSViewWidthSizable | NSViewHeightSizable); + // All this is doing is telling the view/layer to resize when the parent does. + objc_msgSend(metal_view, sel_getUid("setAutoresizingMask:"), 18); + + objc_msgSend(view, sel_getUid("addSubview:"), metal_view); + s_metal_view_handle = metal_view; #endif } } diff --git a/Source/Core/VideoCommon/RenderBase.h b/Source/Core/VideoCommon/RenderBase.h index 45a2900afb..35da0a1aeb 100644 --- a/Source/Core/VideoCommon/RenderBase.h +++ b/Source/Core/VideoCommon/RenderBase.h @@ -149,7 +149,7 @@ class Renderer // Final surface changing // This is called when the surface is resized (WX) or the window changes (Android). virtual void ChangeSurface(void* new_surface_handle) {} - virtual void CacheSurfaceHandle(void* new_surface_handle) {} + virtual void CacheSurfaceHandle(void* new_surface_handle) {} bool UseVertexDepthRange() const; protected: std::tuple CalculateTargetScale(int x, int y) const; From 37c2acd82a580fbfb326d21f50a0c5bcb0b64645 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 13 Aug 2021 12:17:38 -0700 Subject: [PATCH 5/7] Flag MoltenVK ENV variables on Playback The performance difference for Playback isn't so significant that this makes sense to check for Rosetta 2 and change accordingly. --- Source/Core/VideoBackends/Vulkan/main.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Core/VideoBackends/Vulkan/main.cpp b/Source/Core/VideoBackends/Vulkan/main.cpp index 4fe03dfd33..87ea2bfd43 100644 --- a/Source/Core/VideoBackends/Vulkan/main.cpp +++ b/Source/Core/VideoBackends/Vulkan/main.cpp @@ -409,11 +409,18 @@ Class getSLPMetalLayerViewClassType() if (SLPMetalLayerViewClass == nullptr) { +#ifdef IS_PLAYBACK + // These are disabled on Playback builds for now, as M1 devices running Playback under Rosetta 2 + // seem to hit a race condition with asynchronous queue submits. Rendering takes a slight hit but + // this matters less in playback, and it's still better than OpenGL. + //setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0); + //setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); +#else // This does a one-time opt-in to a MVK flag that seems to universally help in Ishiiruka. // (mainline should not need this) setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0); - setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); - + setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); +#endif SLPMetalLayerViewClass = objc_allocateClassPair( (Class)objc_getClass("NSView"), kSLPMetalLayerViewClassName, From e8d2372b263dd84d573abfcd7b890ee784d8c4e6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 21 Nov 2021 16:49:56 -0800 Subject: [PATCH 6/7] Pin ffmpeg in CI due to upstream changes. --- .github/workflows/pr-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 0bf9e3b4d7..74dfddcb41 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -238,7 +238,7 @@ jobs: echo "HOMEBREW_NO_AUTO_UPDATE=1" >> $GITHUB_ENV brew upgrade cmake brew install \ - ffmpeg \ + ffmpeg@4.4.1 \ libpng \ libav \ pkgconfig \ From a3b3adb3d06f37c45d3a961a910e846db371d06a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 21 Nov 2021 17:06:46 -0800 Subject: [PATCH 7/7] Try 2.8 I guess. --- .github/workflows/pr-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 74dfddcb41..a5ceb78e90 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -238,7 +238,7 @@ jobs: echo "HOMEBREW_NO_AUTO_UPDATE=1" >> $GITHUB_ENV brew upgrade cmake brew install \ - ffmpeg@4.4.1 \ + ffmpeg@2.8 \ libpng \ libav \ pkgconfig \