From c0350c54abea1068dfdeaed14e315f1daaa5a1d4 Mon Sep 17 00:00:00 2001 From: Tom Poole Date: Wed, 29 Jun 2022 16:41:12 +0100 Subject: [PATCH] macOS: Fix CGMetalLayerRenderer assertions and resizing --- .../native/juce_ios_UIViewComponentPeer.mm | 33 +--- .../native/juce_mac_CGMetalLayerRenderer.h | 180 +++++++++++------- .../native/juce_mac_NSViewComponentPeer.mm | 65 +++---- 3 files changed, 151 insertions(+), 127 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm index 3a62264678..3121bb84a7 100644 --- a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm @@ -331,7 +331,7 @@ private: } }; - std::unique_ptr metalRenderer; + std::unique_ptr> metalRenderer; RectangleList deferredRepaints; //============================================================================== @@ -534,7 +534,7 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; + (Class) layerClass { #if JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS - if (@available (iOS 12, *)) + if (@available (iOS 13, *)) return [CAMetalLayer class]; #endif @@ -717,8 +717,8 @@ UIViewComponentPeer::UIViewComponentPeer (Component& comp, int windowStyleFlags, view.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent: 0]; #if JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS - if (@available (iOS 12, *)) - metalRenderer = std::make_unique ((CAMetalLayer*) view.layer, comp); + if (@available (iOS 13, *)) + metalRenderer = std::make_unique> (view, comp); #endif if ((windowStyleFlags & ComponentPeer::windowRequiresSynchronousCoreGraphicsRendering) == 0) @@ -1195,26 +1195,13 @@ void UIViewComponentPeer::displayLinkCallback() auto dispatchRectangles = [this] () { - // We shouldn't need this preprocessor guard, but when running in the simulator - // CAMetalLayer is flagged as requiring iOS 13 - #if JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS if (metalRenderer != nullptr) - { - if (@available (iOS 12, *)) - { - return metalRenderer->drawRectangleList ((CAMetalLayer*) view.layer, - (float) view.contentScaleFactor, - view.frame, - component, - [this] (CGContextRef ctx, CGRect r) { drawRectWithContext (ctx, r); }, - deferredRepaints); - } - - // The creation of metalRenderer should already be guarded with @available (iOS 12, *). - jassertfalse; - return false; - } - #endif + return metalRenderer->drawRectangleList (view, + (float) view.contentScaleFactor, + view.frame, + component, + [this] (CGContextRef ctx, CGRect r) { drawRectWithContext (ctx, r); }, + deferredRepaints); for (const auto& r : deferredRepaints) [view setNeedsDisplayInRect: convertToCGRect (r)]; diff --git a/modules/juce_gui_basics/native/juce_mac_CGMetalLayerRenderer.h b/modules/juce_gui_basics/native/juce_mac_CGMetalLayerRenderer.h index c451751a6d..f09f293117 100644 --- a/modules/juce_gui_basics/native/juce_mac_CGMetalLayerRenderer.h +++ b/modules/juce_gui_basics/native/juce_mac_CGMetalLayerRenderer.h @@ -30,13 +30,37 @@ namespace juce { //============================================================================== +template class CoreGraphicsMetalLayerRenderer { public: //============================================================================== - CoreGraphicsMetalLayerRenderer (CAMetalLayer* layer, const Component& comp) + CoreGraphicsMetalLayerRenderer (ViewType* view, const Component& comp) { device.reset (MTLCreateSystemDefaultDevice()); + commandQueue.reset ([device.get() newCommandQueue]); + + attach (view, comp); + } + + ~CoreGraphicsMetalLayerRenderer() + { + if (memoryBlitCommandBuffer != nullptr) + { + stopGpuCommandSubmission = true; + [memoryBlitCommandBuffer.get() waitUntilCompleted]; + } + } + + void attach (ViewType* view, const Component& comp) + { + #if JUCE_MAC + view.wantsLayer = YES; + view.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft; + view.layer = [CAMetalLayer layer]; + #endif + + auto layer = (CAMetalLayer*) view.layer; layer.device = device.get(); layer.framebufferOnly = NO; @@ -44,23 +68,35 @@ public: layer.opaque = comp.isOpaque(); layer.allowsNextDrawableTimeout = NO; - commandQueue.reset ([device.get() newCommandQueue]); + attachedView = view; + doSynchronousRender = true; } - ~CoreGraphicsMetalLayerRenderer() + void detach() { - stopGpuCommandSubmission = true; - [memoryBlitCommandBuffer.get() waitUntilCompleted]; + #if JUCE_MAC + attachedView.wantsLayer = NO; + attachedView.layer = nil; + #endif + + attachedView = nullptr; + } + + bool isAttachedToView (ViewType* view) const + { + return view == attachedView && attachedView != nullptr; } template - bool drawRectangleList (CAMetalLayer* layer, + bool drawRectangleList (ViewType* view, float scaleFactor, CGRect viewFrame, const Component& comp, Callback&& drawRectWithContext, const RectangleList& dirtyRegions) { + auto layer = (CAMetalLayer*) view.layer; + if (memoryBlitCommandBuffer != nullptr) { switch ([memoryBlitCommandBuffer.get() status]) @@ -85,7 +121,7 @@ public: const auto componentHeight = comp.getHeight(); - if (! CGSizeEqualToSize (layer.drawableSize, transformedFrameSize)) + if (resources == nullptr || ! CGSizeEqualToSize (layer.drawableSize, transformedFrameSize)) { layer.drawableSize = transformedFrameSize; resources = std::make_unique (device.get(), layer, componentHeight); @@ -117,62 +153,75 @@ public: auto sharedTexture = resources->getSharedTexture(); - memoryBlitCommandBuffer.reset ([commandQueue.get() commandBuffer]); - - // Command buffers are usually considered temporary, and are automatically released by - // the operating system when the rendering pipeline is finsihed. However, we want to keep - // this one alive so that we can wait for pipeline completion in the destructor. - [memoryBlitCommandBuffer.get() retain]; - - auto blitCommandEncoder = [memoryBlitCommandBuffer.get() blitCommandEncoder]; - [blitCommandEncoder copyFromTexture: sharedTexture - sourceSlice: 0 - sourceLevel: 0 - sourceOrigin: MTLOrigin{} - sourceSize: MTLSize { sharedTexture.width, sharedTexture.height, 1 } - toTexture: gpuTexture - destinationSlice: 0 - destinationLevel: 0 - destinationOrigin: MTLOrigin{}]; - [blitCommandEncoder endEncoding]; - - [memoryBlitCommandBuffer.get() addScheduledHandler: ^(id) + auto encodeBlit = [] (id commandBuffer, + id source, + id destination) { - // We're on a Metal thread, so we can make a blocking nextDrawable call - // without stalling the message thread. - - // Check if we can do an early exit. - if (stopGpuCommandSubmission) - return; + auto blitCommandEncoder = [commandBuffer blitCommandEncoder]; + [blitCommandEncoder copyFromTexture: source + sourceSlice: 0 + sourceLevel: 0 + sourceOrigin: MTLOrigin{} + sourceSize: MTLSize { source.width, source.height, 1 } + toTexture: destination + destinationSlice: 0 + destinationLevel: 0 + destinationOrigin: MTLOrigin{}]; + [blitCommandEncoder endEncoding]; + }; + if (doSynchronousRender) + { @autoreleasepool { + id commandBuffer = [commandQueue.get() commandBuffer]; + id drawable = [layer nextDrawable]; + encodeBlit (commandBuffer, sharedTexture, drawable.texture); - id presentationCommandBuffer = [commandQueue.get() commandBuffer]; - - auto presentationBlitCommandEncoder = [presentationCommandBuffer blitCommandEncoder]; - [presentationBlitCommandEncoder copyFromTexture: gpuTexture - sourceSlice: 0 - sourceLevel: 0 - sourceOrigin: MTLOrigin{} - sourceSize: MTLSize { gpuTexture.width, gpuTexture.height, 1 } - toTexture: drawable.texture - destinationSlice: 0 - destinationLevel: 0 - destinationOrigin: MTLOrigin{}]; - [presentationBlitCommandEncoder endEncoding]; - - [presentationCommandBuffer addScheduledHandler: ^(id) - { - [drawable present]; - }]; - - [presentationCommandBuffer commit]; + [commandBuffer presentDrawable: drawable]; + [commandBuffer commit]; } - }]; - [memoryBlitCommandBuffer.get() commit]; + doSynchronousRender = false; + } + else + { + // Command buffers are usually considered temporary, and are automatically released by + // the operating system when the rendering pipeline is finsihed. However, we want to keep + // this one alive so that we can wait for pipeline completion in the destructor. + memoryBlitCommandBuffer.reset ([[commandQueue.get() commandBuffer] retain]); + + encodeBlit (memoryBlitCommandBuffer.get(), sharedTexture, gpuTexture); + + [memoryBlitCommandBuffer.get() addScheduledHandler: ^(id) + { + // We're on a Metal thread, so we can make a blocking nextDrawable call + // without stalling the message thread. + + // Check if we can do an early exit. + if (stopGpuCommandSubmission) + return; + + @autoreleasepool + { + id drawable = [layer nextDrawable]; + + id presentationCommandBuffer = [commandQueue.get() commandBuffer]; + + encodeBlit (presentationCommandBuffer, gpuTexture, drawable.texture); + + [presentationCommandBuffer addScheduledHandler: ^(id) + { + [drawable present]; + }]; + + [presentationCommandBuffer commit]; + } + }]; + + [memoryBlitCommandBuffer.get() commit]; + } return true; } @@ -184,18 +233,6 @@ private: return ((n + alignment - 1) / alignment) * alignment; } - //============================================================================== - struct TextureDeleter - { - void operator() (id texture) const noexcept - { - [texture setPurgeableState: MTLPurgeableStateEmpty]; - [texture release]; - } - }; - - using TextureUniquePtr = std::unique_ptr>, TextureDeleter>; - //============================================================================== class GpuTexturePool { @@ -209,12 +246,12 @@ private: id take() const { auto iter = std::find_if (textureCache.begin(), textureCache.end(), - [] (const TextureUniquePtr& t) { return [t.get() retainCount] == 1; }); + [] (const ObjCObjectHandle>& t) { return [t.get() retainCount] == 1; }); return iter == textureCache.end() ? nullptr : (*iter).get(); } private: - std::array textureCache; + std::array>, 3> textureCache; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuTexturePool) JUCE_DECLARE_NON_MOVEABLE (GpuTexturePool) @@ -339,7 +376,7 @@ private: detail::ContextPtr cgContext; ObjCObjectHandle> buffer; - TextureUniquePtr sharedTexture; + ObjCObjectHandle> sharedTexture; std::unique_ptr gpuTexturePool; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Resources) @@ -347,6 +384,9 @@ private: }; //============================================================================== + ViewType* attachedView = nullptr; + bool doSynchronousRender = false; + std::unique_ptr resources; ObjCObjectHandle> device; diff --git a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm index 0725c6d3e8..5d15acf600 100644 --- a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm @@ -153,7 +153,11 @@ public: [view setPostsFrameChangedNotifications: YES]; - #if USE_COREGRAPHICS_RENDERING + #if USE_COREGRAPHICS_RENDERING + #if JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS + if (@available (macOS 10.14, *)) + metalRenderer = std::make_unique> (view, getComponent()); + #endif if ((windowStyleFlags & ComponentPeer::windowRequiresSynchronousCoreGraphicsRendering) == 0) { if (@available (macOS 10.8, *)) @@ -162,7 +166,7 @@ public: [[view layer] setDrawsAsynchronously: YES]; } } - #endif + #endif createCVDisplayLink(); @@ -362,7 +366,10 @@ public: } if (oldViewSize.width != r.size.width || oldViewSize.height != r.size.height) + { + numFramesToSkipMetalRenderer = 5; [view setNeedsDisplay: true]; + } } Rectangle getBounds (const bool global) const @@ -1076,52 +1083,41 @@ public: if (msSinceLastRepaint < minimumRepaintInterval && shouldThrottleRepaint()) return; - #if USE_COREGRAPHICS_RENDERING && JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS - // We require macOS 10.14 to use the Metal layer renderer - if (@available (macOS 10.14, *)) + if (metalRenderer != nullptr) { - const auto& comp = getComponent(); + const auto compBounds = getComponent().getLocalBounds().toFloat(); // If we are resizing we need to fall back to synchronous drawing to avoid artefacts - if (areAnyWindowsInLiveResize()) + if ([window inLiveResize] || numFramesToSkipMetalRenderer > 0) { - if (metalRenderer != nullptr) + if (metalRenderer->isAttachedToView (view)) { - metalRenderer.reset(); - view.wantsLayer = NO; - view.layer = nil; - deferredRepaints = comp.getLocalBounds().toFloat(); + metalRenderer->detach(); + deferredRepaints = compBounds; } + + if (numFramesToSkipMetalRenderer > 0) + --numFramesToSkipMetalRenderer; } else { - if (metalRenderer == nullptr) + if (! metalRenderer->isAttachedToView (view)) { - view.wantsLayer = YES; - view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; - view.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft; - view.layer = [CAMetalLayer layer]; - metalRenderer = std::make_unique ((CAMetalLayer*) view.layer, getComponent()); - deferredRepaints = comp.getLocalBounds().toFloat(); + metalRenderer->attach (view, getComponent()); + deferredRepaints = compBounds; } } } - #endif - auto dispatchRectangles = [this] () + auto dispatchRectangles = [this] { - if (@available (macOS 10.14, *)) - { - if (metalRenderer != nullptr) - { - return metalRenderer->drawRectangleList ((CAMetalLayer*) view.layer, - (float) [[view window] backingScaleFactor], - view.frame, - getComponent(), - [this] (CGContextRef ctx, CGRect r) { drawRectWithContext (ctx, r); }, - deferredRepaints); - } - } + if (metalRenderer != nullptr && metalRenderer->isAttachedToView (view)) + return metalRenderer->drawRectangleList (view, + (float) [[view window] backingScaleFactor], + view.frame, + getComponent(), + [this] (CGContextRef ctx, CGRect r) { drawRectWithContext (ctx, r); }, + deferredRepaints); for (auto& i : deferredRepaints) [view setNeedsDisplayInRect: makeNSRect (i)]; @@ -1893,7 +1889,8 @@ private: CVDisplayLinkRef displayLink = nullptr; dispatch_source_t displaySource = nullptr; - std::unique_ptr metalRenderer; + int numFramesToSkipMetalRenderer = 0; + std::unique_ptr> metalRenderer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NSViewComponentPeer) };