diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index ac1b7de9924..94cc6867249 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -415,6 +415,13 @@ qt_internal_extend_target(Gui CONDITION APPLE ${FWImageIO} ) +qt_internal_extend_target(Gui CONDITION APPLE AND QT_FEATURE_metal + SOURCES + platform/darwin/qmetallayer.mm platform/darwin/qmetallayer_p.h + LIBRARIES + ${FWQuartzCore} +) + qt_internal_extend_target(Gui CONDITION QNX SOURCES painting/qrasterbackingstore.cpp painting/qrasterbackingstore_p.h diff --git a/src/gui/kernel/qplatformwindow_p.h b/src/gui/kernel/qplatformwindow_p.h index 2bbdfd5bf9f..0bbddea7227 100644 --- a/src/gui/kernel/qplatformwindow_p.h +++ b/src/gui/kernel/qplatformwindow_p.h @@ -58,6 +58,8 @@ struct Q_GUI_EXPORT QCocoaWindow QT_DECLARE_NATIVE_INTERFACE(QCocoaWindow, 1, QWindow) virtual void setContentBorderEnabled(bool enable) = 0; virtual QPoint bottomLeftClippedByNSWindowOffset() const = 0; + + virtual bool inLiveResize() const = 0; }; #endif diff --git a/src/gui/platform/darwin/qmetallayer.mm b/src/gui/platform/darwin/qmetallayer.mm new file mode 100644 index 00000000000..e8a27a7b067 --- /dev/null +++ b/src/gui/platform/darwin/qmetallayer.mm @@ -0,0 +1,73 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qmetallayer_p.h" + +#include +#include + +using namespace std::chrono_literals; + +QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcMetalLayer, "qt.gui.metal") +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@implementation QMetalLayer +{ + std::unique_ptr m_displayLock; +} + +- (instancetype)init +{ + if ((self = [super init])) { + m_displayLock.reset(new QReadWriteLock(QReadWriteLock::Recursive)); + self.mainThreadPresentation = nil; + } + + return self; +} + +- (QReadWriteLock &)displayLock +{ + return *m_displayLock.get(); +} + +- (void)setNeedsDisplay +{ + [self setNeedsDisplayInRect:CGRectInfinite]; +} + +- (void)setNeedsDisplayInRect:(CGRect)rect +{ + if (!self.needsDisplay) { + // We lock for writing here, blocking in case a secondary thread is in + // the middle of presenting to the layer, as we want the main thread's + // display to happen after the secondary thread finishes presenting. + qCDebug(lcMetalLayer) << "Locking" << self << "for writing" + << "due to needing display in rect" << QRectF::fromCGRect(rect); + + // For added safety, we use a 5 second timeout, and try to fail + // gracefully by not marking the layer as needing display, as + // doing so would lead us to unlock and unheld lock in displayLayer. + if (!self.displayLock.tryLockForWrite(5s)) { + qCWarning(lcMetalLayer) << "Timed out waiting for display lock"; + return; + } + } + + [super setNeedsDisplayInRect:rect]; +} + +- (id)nextDrawable +{ + // Drop the presentation block early, so that if the main thread for + // some reason doesn't handle the presentation, the block won't hold on + // to a drawable unnecessarily. + self.mainThreadPresentation = nil; + return [super nextDrawable]; +} + + +@end diff --git a/src/gui/platform/darwin/qmetallayer_p.h b/src/gui/platform/darwin/qmetallayer_p.h new file mode 100644 index 00000000000..1c19f218665 --- /dev/null +++ b/src/gui/platform/darwin/qmetallayer_p.h @@ -0,0 +1,41 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMETALLAYER_P_H +#define QMETALLAYER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE +class QReadWriteLock; + +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(lcMetalLayer, Q_GUI_EXPORT) + +QT_END_NAMESPACE + +#if defined(__OBJC__) +Q_GUI_EXPORT +#endif +QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QMetalLayer, CAMetalLayer +@property (nonatomic, readonly) QT_PREPEND_NAMESPACE(QReadWriteLock) &displayLock; +@property (atomic, copy) void (^mainThreadPresentation)(); +) + +#endif // QMETALLAYER_P_H diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index 9fadfc15fa0..887d40c7a59 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -13,6 +13,7 @@ #include #include +#include #ifdef Q_OS_MACOS #include @@ -20,8 +21,9 @@ #include #endif +#include + #include -#include QT_BEGIN_NAMESPACE @@ -461,11 +463,6 @@ struct QMetalSwapChainData id msaaTex[QMTL_FRAMES_IN_FLIGHT]; QRhiTexture::Format rhiColorFormat; MTLPixelFormat colorFormat; -#ifdef Q_OS_MACOS - bool liveResizeObserverSet = false; - QMacNotificationObserver liveResizeStartObserver; - QMacNotificationObserver liveResizeEndObserver; -#endif }; QRhiMetal::QRhiMetal(QRhiMetalInitParams *params, QRhiMetalNativeHandles *importDevice) @@ -2396,8 +2393,11 @@ QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrame QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, swapChain); Q_ASSERT(currentSwapChain == swapChainD); + // Keep strong reference to command buffer + id commandBuffer = swapChainD->cbWrapper.d->cb; + __block int thisFrameSlot = currentFrameSlot; - [swapChainD->cbWrapper.d->cb addCompletedHandler: ^(id cb) { + [commandBuffer addCompletedHandler: ^(id cb) { swapChainD->d->lastGpuTime[thisFrameSlot] += cb.GPUEndTime - cb.GPUStartTime; dispatch_semaphore_signal(swapChainD->d->sem[thisFrameSlot]); }]; @@ -2407,30 +2407,75 @@ QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrame // released before the command buffer is done with it. Manually keep it alive // to work around this. id drawableTexture = [swapChainD->d->curDrawable.texture retain]; - [swapChainD->cbWrapper.d->cb addCompletedHandler:^(id) { + [commandBuffer addCompletedHandler:^(id) { [drawableTexture release]; }]; #endif - const bool needsPresent = !flags.testFlag(QRhi::SkipPresent); - const bool presentsWithTransaction = swapChainD->d->layer.presentsWithTransaction; - if (!presentsWithTransaction && needsPresent) { - // beginFrame-endFrame without a render pass inbetween means there is no drawable. - if (id drawable = swapChainD->d->curDrawable) - [swapChainD->cbWrapper.d->cb presentDrawable: drawable]; - } - - [swapChainD->cbWrapper.d->cb commit]; - - if (presentsWithTransaction && needsPresent) { - // beginFrame-endFrame without a render pass inbetween means there is no drawable. + if (flags.testFlag(QRhi::SkipPresent)) { + // Just need to commit, that's it + [commandBuffer commit]; + } else { if (id drawable = swapChainD->d->curDrawable) { - // The layer has presentsWithTransaction set to true to avoid flicker on resizing, - // so here it is important to follow what the Metal docs say when it comes to the - // issuing the present. - [swapChainD->cbWrapper.d->cb waitUntilScheduled]; - [drawable present]; + // Got something to present + if (swapChainD->d->layer.presentsWithTransaction) { + [commandBuffer commit]; + // Keep strong reference to Metal layer + auto *metalLayer = swapChainD->d->layer; + auto presentWithTransaction = ^{ + [commandBuffer waitUntilScheduled]; + // If the layer has been resized while we waited to be scheduled we bail out, + // as the drawable is no longer valid for the layer, and we'll get a follow-up + // display with the right size. We know we are on the main thread here, which + // means we can access the layer directly. We also know that the layer is valid, + // since the block keeps a strong reference to it, compared to the QRhiSwapChain + // that can go away under our feet by the time we're scheduled. + const auto surfaceSize = QSizeF::fromCGSize(metalLayer.bounds.size) * metalLayer.contentsScale; + const auto textureSize = QSizeF(drawable.texture.width, drawable.texture.height); + if (textureSize == surfaceSize) { + [drawable present]; + } else { + qCDebug(QRHI_LOG_INFO) << "Skipping" << drawable << "due to texture size" + << textureSize << "not matching surface size" << surfaceSize; + } + }; + + if (NSThread.currentThread == NSThread.mainThread) { + presentWithTransaction(); + } else { + auto *qtMetalLayer = qt_objc_cast(swapChainD->d->layer); + Q_ASSERT(qtMetalLayer); + // Let the main thread present the drawable from displayLayer + qtMetalLayer.mainThreadPresentation = presentWithTransaction; + } + } else { + // Keep strong reference to Metal layer so it's valid in the block + auto *qtMetalLayer = qt_objc_cast(swapChainD->d->layer); + [commandBuffer addScheduledHandler:^(id) { + if (qtMetalLayer) { + // The schedule handler comes in on the com.Metal.CompletionQueueDispatch + // thread, which means we might be racing against a display cycle on the + // main thread. If the displayLayer is already in progress, we don't want + // to step on its toes. + if (qtMetalLayer.displayLock.tryLockForRead()) { + [drawable present]; + qtMetalLayer.displayLock.unlock(); + } else { + qCDebug(QRHI_LOG_INFO) << "Skipping" << drawable + << "due to" << qtMetalLayer << "needing display"; + } + } else { + [drawable present]; + } + }]; + [commandBuffer commit]; + } + } else { + // Still need to commit, even if we don't have a drawable + [commandBuffer commit]; } + + swapChainD->currentFrameSlot = (swapChainD->currentFrameSlot + 1) % QMTL_FRAMES_IN_FLIGHT; } // Must not hold on to the drawable, regardless of needsPresent @@ -2439,9 +2484,6 @@ QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrame [d->captureScope endScope]; - if (needsPresent) - swapChainD->currentFrameSlot = (swapChainD->currentFrameSlot + 1) % QMTL_FRAMES_IN_FLIGHT; - swapChainD->frameCount += 1; currentSwapChain = nullptr; return QRhi::FrameOpSuccess; @@ -6168,12 +6210,6 @@ void QMetalSwapChain::destroy() d->msaaTex[i] = nil; } -#ifdef Q_OS_MACOS - d->liveResizeStartObserver.remove(); - d->liveResizeEndObserver.remove(); - d->liveResizeObserverSet = false; -#endif - d->layer = nullptr; m_proxyData = {}; @@ -6380,34 +6416,6 @@ bool QMetalSwapChain::createOrResize() [d->layer setDevice: rhiD->d->dev]; -#ifdef Q_OS_MACOS - // Can only use presentsWithTransaction (to get smooth resizing) when - // presenting from the main (gui) thread. We predict that based on the - // thread this function is called on since if the QRhiSwapChain is - // initialied on a given thread then that's almost certainly the thread on - // which the QRhi renders and presents. - const bool canUsePresentsWithTransaction = NSThread.isMainThread; - - // Have an env.var. just in case it turns out presentsWithTransaction is - // not desired in some specific case. - static bool allowPresentsWithTransaction = !qEnvironmentVariableIntValue("QT_MTL_NO_TRANSACTION"); - - if (allowPresentsWithTransaction && canUsePresentsWithTransaction && !d->liveResizeObserverSet) { - d->liveResizeObserverSet = true; - NSView *view = reinterpret_cast(window->winId()); - NSWindow *window = view.window; - if (window) { - qCDebug(QRHI_LOG_INFO, "will set presentsWithTransaction during live resize"); - d->liveResizeStartObserver = QMacNotificationObserver(window, NSWindowWillStartLiveResizeNotification, [this] { - d->layer.presentsWithTransaction = true; - }); - d->liveResizeEndObserver = QMacNotificationObserver(window, NSWindowDidEndLiveResizeNotification, [this] { - d->layer.presentsWithTransaction = false; - }); - } - } -#endif - [d->curDrawable release]; d->curDrawable = nil; diff --git a/src/plugins/platforms/cocoa/qcocoascreen.mm b/src/plugins/platforms/cocoa/qcocoascreen.mm index be562e54553..2249658189e 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -471,25 +471,6 @@ void QCocoaScreen::deliverUpdateRequests() if (!platformWindow->updatesWithDisplayLink()) continue; - // QTBUG-107198: Skip updates in a live resize for a better resize experience. - if (platformWindow->isContentView() && platformWindow->view().inLiveResize) { - const QSurface::SurfaceType surfaceType = window->surfaceType(); - const bool usesMetalLayer = surfaceType == QWindow::MetalSurface || surfaceType == QWindow::VulkanSurface; - const bool usesNonDefaultContentsPlacement = [platformWindow->view() layerContentsPlacement] - != NSViewLayerContentsPlacementScaleAxesIndependently; - if (usesMetalLayer && usesNonDefaultContentsPlacement) { - static bool deliverDisplayLinkUpdatesDuringLiveResize = - qEnvironmentVariableIsSet("QT_MAC_DISPLAY_LINK_UPDATE_IN_RESIZE"); - if (!deliverDisplayLinkUpdatesDuringLiveResize) { - // Must keep the link running, we do not know what the event - // handlers for UpdateRequest (which is not sent now) would do, - // would they trigger a new requestUpdate() or not. - pauseUpdates = false; - continue; - } - } - } - platformWindow->deliverUpdateRequest(); // Another update request was triggered, keep the display link running diff --git a/src/plugins/platforms/cocoa/qcocoawindow.h b/src/plugins/platforms/cocoa/qcocoawindow.h index c6b4d83915b..2036d4bf4c8 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.h +++ b/src/plugins/platforms/cocoa/qcocoawindow.h @@ -124,6 +124,7 @@ public: Q_NOTIFICATION_HANDLER(NSWindowDidMoveNotification) void windowDidMove(); Q_NOTIFICATION_HANDLER(NSWindowDidResizeNotification) void windowDidResize(); + Q_NOTIFICATION_HANDLER(NSWindowWillStartLiveResizeNotification) void windowWillStartLiveResize(); Q_NOTIFICATION_HANDLER(NSWindowDidEndLiveResizeNotification) void windowDidEndLiveResize(); Q_NOTIFICATION_HANDLER(NSWindowDidBecomeKeyNotification) void windowDidBecomeKey(); Q_NOTIFICATION_HANDLER(NSWindowDidResignKeyNotification) void windowDidResignKey(); @@ -188,6 +189,8 @@ public: Q_DECLARE_FLAGS(RecreationReasons, RecreationReason) Q_FLAG(RecreationReasons) + bool inLiveResize() const override; + protected: void recreateWindowIfNeeded(); QCocoaNSWindow *createNSWindow(bool shouldBePanel); @@ -234,6 +237,7 @@ public: // for QNSView bool m_inSetVisible = false; bool m_inSetGeometry = false; bool m_inSetStyleMask = false; + bool m_inLiveResize = false; QCocoaMenuBar *m_menubar = nullptr; diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 37df66d7bdd..b4c8ae4ba1f 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -24,6 +24,7 @@ #include #include #include +#include #include @@ -1284,8 +1285,26 @@ void QCocoaWindow::windowDidResize() handleWindowStateChanged(); } +void QCocoaWindow::windowWillStartLiveResize() +{ + // Track live resizing for all windows, including + // child windows, so we know if it's safe to update + // the window unthrottled outside of the main thread. + m_inLiveResize = true; +} + +bool QCocoaWindow::inLiveResize() const +{ + // Use member variable to track this instead of reflecting + // NSView.inLiveResize directly, so it can be called from + // non-main threads. + return m_inLiveResize; +} + void QCocoaWindow::windowDidEndLiveResize() { + m_inLiveResize = false; + if (!isContentView()) return; @@ -1671,6 +1690,23 @@ bool QCocoaWindow::updatesWithDisplayLink() const void QCocoaWindow::deliverUpdateRequest() { qCDebug(lcQpaDrawing) << "Delivering update request to" << window(); + + if (auto *qtMetalLayer = qt_objc_cast(m_view.layer)) { + // We attempt a read lock here, so that the animation/render thread is + // prioritized lower than the main thread's displayLayer processing. + // Without this the two threads might fight over the next drawable, + // starving the main thread's presentation of the resized layer. + if (!qtMetalLayer.displayLock.tryLockForRead()) { + qCDebug(lcQpaDrawing) << "Deferring update request" + << "due to" << qtMetalLayer << "needing display"; + return; + } + + // But we don't hold the lock, as the update request can recurse + // back into setNeedsDisplay, which would deadlock. + qtMetalLayer.displayLock.unlock(); + } + QPlatformWindow::deliverUpdateRequest(); } diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index 48ffa5c1cc8..c63a8e28790 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -36,6 +36,9 @@ #endif #include "qcocoaintegration.h" #include +#include + +#include @interface QNSView (Drawing) - (void)initDrawing; diff --git a/src/plugins/platforms/cocoa/qnsview_drawing.mm b/src/plugins/platforms/cocoa/qnsview_drawing.mm index bf102e43f85..61691ab4fb5 100644 --- a/src/plugins/platforms/cocoa/qnsview_drawing.mm +++ b/src/plugins/platforms/cocoa/qnsview_drawing.mm @@ -75,7 +75,10 @@ // too late at this point and the QWindow will be non-functional, // but we can at least print a warning. if ([MTLCreateSystemDefaultDevice() autorelease]) { - return [CAMetalLayer layer]; + static bool allowPresentsWithTransaction = + !qEnvironmentVariableIsSet("QT_MTL_NO_TRANSACTION"); + return allowPresentsWithTransaction ? + [QMetalLayer layer] : [CAMetalLayer layer]; } else { qCWarning(lcQpaDrawing) << "Failed to create QWindow::MetalSurface." << "Metal is not supported by any of the GPUs in this system."; @@ -222,8 +225,39 @@ return; } - qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window(); - m_platformWindow->handleExposeEvent(QRectF::fromCGRect(self.bounds).toRect()); + const auto handleExposeEvent = [&]{ + const auto bounds = QRectF::fromCGRect(self.bounds).toRect(); + qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window() << bounds; + m_platformWindow->handleExposeEvent(bounds); + }; + + if (auto *qtMetalLayer = qt_objc_cast(self.layer)) { + const bool presentedWithTransaction = qtMetalLayer.presentsWithTransaction; + qtMetalLayer.presentsWithTransaction = YES; + + handleExposeEvent(); + + // If the expose event resulted in a secondary thread requesting that its + // drawable should be presented on the main thread with transaction, do so. + if (auto mainThreadPresentation = qtMetalLayer.mainThreadPresentation) { + mainThreadPresentation(); + qtMetalLayer.mainThreadPresentation = nil; + } + + qtMetalLayer.presentsWithTransaction = presentedWithTransaction; + + // We're done presenting, but we must wait to unlock the display lock + // until the display cycle finishes, as otherwise the render thread may + // step in and present before the transaction commits. The display lock + // is recursive, so setNeedsDisplay can be safely called in the meantime + // without any issue. + QMetaObject::invokeMethod(m_platformWindow, [qtMetalLayer]{ + qCDebug(lcMetalLayer) << "Unlocking" << qtMetalLayer << "after finishing display-cycle"; + qtMetalLayer.displayLock.unlock(); + }, Qt::QueuedConnection); + } else { + handleExposeEvent(); + } } @end