macOS: Present Metal layer with transaction during display cycle
When our Metal layer changes its bounds or contents scale, for example as a result of a window resize, or moving a window between screens of different device-pixel-ratios, the layer will be marked as needing display. The changes to the layer will then make their way to the screen as part of committing the root level CATransaction, managed by the macOS display cycle, which synchronizes the presentation of the bounds or scale changes to the screen's vertical sync. By default, presenting a Metal drawable to a Metal layer ignores any ongoing transactions, so the drawable ends up on the screen as fast as possible. The downside to this is that the drawable size or scale may be based on the yet-to-be presented bounds or scale changes of the layer, as these happen when the display cycle commits its transaction. To ensure that the layer properties and content changes in lock step we can enable the presentsWithTransaction property on the Metal layer. We do so selectively when the layer is asked to display, which happens during the display cycle, but not for update requests (via our display-link). This would normally be enough to ensure smooth and glitch free resizing, as long as everything happens on the main thread. Unfortunately, the [MTLCommandBuffer presentDrawable:] API we use to present a Metal drawable when rendering in response to update requests is actually scheduling the presentation on a secondary thread com.Metal.CompletionQueueDispatch queue. The result of this is that the presentation on the secondary thread might race against a presentation on the main thread initiated from the display-cycle, presenting too early, or overwriting the layer's content with stale content. To fix this we use [MTLCommandBuffer addScheduledHandler:] explicitly instead, which lets us control what happens during the presentation on the secondary thread. We then add a lock to the layer that we lock as soon as the layer needs display, and use this lock to skip presentations that should not step on the toes of the display-cycle presentation. Once the display cycle ends we unlock the lock. The lock is a read-write lock, to ensure we prioritize the main thread's display-cycle over any other presentations in case of contention. We also defer update requests if we detect that the lock is held, as there is no point in rendering a frame if we are likely not going to present it. Doing this also prevents the update-requests from starving the main thread from getting its drawables. The final case we have to account for is where the display of the layer during the display-cycle is implemented by asking another thread to do the rendering, as is the case with the Qt Quick threaded render loop. In this case the main thread is blocked while it waits for the render thread to complete drawing and presenting a frame. But the actual presentation of the Metal drawable still has to happen on the Main thread for it to be part of the display-cycle's transaction. To ensure the latter, we move the presentation of the drawable to a block, that we schedule to be run on the main thread. Once displayLayer is done with the expose event it processes the deferred presentation, on the main thread. Finally, to mitigate Qt Quick's threaded renderer running animators without any coordination with the main thread, and thereby starving it from drawables, we expose the inLiveResize property on QNativeInterface::Private::QCocoaWindow, so that the threaded render loop can step back during live resizing and drive animators via the main thread's update request. The QT_MTL_NO_TRANSACTION environment variable has been kept, as an opt-out in case the new machinery breaks somehow. It will disable the locked Metal layer, and all code paths that depend on it. [ChangeLog][macOS] Metal layers are now presented with transactions during the display-cycle, which should fix issues with the layer's content being out of sync with the layer bounds or scale. If this causes issues, set the QT_MTL_NO_TRANSACTION environment variable to opt out. Fixes: QTBUG-107198 Fixes: QTBUG-114351 Change-Id: I765e11051c3a4d44b60ff10e787589feec8917a0 Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io>
This commit is contained in:
parent
eb4cb71925
commit
9122d826d2
@ -415,6 +415,13 @@ qt_internal_extend_target(Gui CONDITION APPLE
|
|||||||
${FWImageIO}
|
${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
|
qt_internal_extend_target(Gui CONDITION QNX
|
||||||
SOURCES
|
SOURCES
|
||||||
painting/qrasterbackingstore.cpp painting/qrasterbackingstore_p.h
|
painting/qrasterbackingstore.cpp painting/qrasterbackingstore_p.h
|
||||||
|
@ -58,6 +58,8 @@ struct Q_GUI_EXPORT QCocoaWindow
|
|||||||
QT_DECLARE_NATIVE_INTERFACE(QCocoaWindow, 1, QWindow)
|
QT_DECLARE_NATIVE_INTERFACE(QCocoaWindow, 1, QWindow)
|
||||||
virtual void setContentBorderEnabled(bool enable) = 0;
|
virtual void setContentBorderEnabled(bool enable) = 0;
|
||||||
virtual QPoint bottomLeftClippedByNSWindowOffset() const = 0;
|
virtual QPoint bottomLeftClippedByNSWindowOffset() const = 0;
|
||||||
|
|
||||||
|
virtual bool inLiveResize() const = 0;
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
73
src/gui/platform/darwin/qmetallayer.mm
Normal file
73
src/gui/platform/darwin/qmetallayer.mm
Normal file
@ -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 <QtCore/qreadwritelock.h>
|
||||||
|
#include <QtCore/qrect.h>
|
||||||
|
|
||||||
|
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<QReadWriteLock> 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<CAMetalDrawable>)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
|
41
src/gui/platform/darwin/qmetallayer_p.h
Normal file
41
src/gui/platform/darwin/qmetallayer_p.h
Normal file
@ -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 <QtGui/qtguiglobal.h>
|
||||||
|
|
||||||
|
#include <QtCore/qreadwritelock.h>
|
||||||
|
#include <QtCore/qloggingcategory.h>
|
||||||
|
#include <QtCore/private/qcore_mac_p.h>
|
||||||
|
|
||||||
|
#include <QuartzCore/CAMetalLayer.h>
|
||||||
|
|
||||||
|
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
|
@ -13,6 +13,7 @@
|
|||||||
#include <QOperatingSystemVersion>
|
#include <QOperatingSystemVersion>
|
||||||
|
|
||||||
#include <QtCore/private/qcore_mac_p.h>
|
#include <QtCore/private/qcore_mac_p.h>
|
||||||
|
#include <QtGui/private/qmetallayer_p.h>
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
#include <AppKit/AppKit.h>
|
#include <AppKit/AppKit.h>
|
||||||
@ -20,8 +21,9 @@
|
|||||||
#include <UIKit/UIKit.h>
|
#include <UIKit/UIKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <QuartzCore/CATransaction.h>
|
||||||
|
|
||||||
#include <Metal/Metal.h>
|
#include <Metal/Metal.h>
|
||||||
#include <QuartzCore/CAMetalLayer.h>
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
@ -461,11 +463,6 @@ struct QMetalSwapChainData
|
|||||||
id<MTLTexture> msaaTex[QMTL_FRAMES_IN_FLIGHT];
|
id<MTLTexture> msaaTex[QMTL_FRAMES_IN_FLIGHT];
|
||||||
QRhiTexture::Format rhiColorFormat;
|
QRhiTexture::Format rhiColorFormat;
|
||||||
MTLPixelFormat colorFormat;
|
MTLPixelFormat colorFormat;
|
||||||
#ifdef Q_OS_MACOS
|
|
||||||
bool liveResizeObserverSet = false;
|
|
||||||
QMacNotificationObserver liveResizeStartObserver;
|
|
||||||
QMacNotificationObserver liveResizeEndObserver;
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
QRhiMetal::QRhiMetal(QRhiMetalInitParams *params, QRhiMetalNativeHandles *importDevice)
|
QRhiMetal::QRhiMetal(QRhiMetalInitParams *params, QRhiMetalNativeHandles *importDevice)
|
||||||
@ -2396,8 +2393,11 @@ QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrame
|
|||||||
QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, swapChain);
|
QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, swapChain);
|
||||||
Q_ASSERT(currentSwapChain == swapChainD);
|
Q_ASSERT(currentSwapChain == swapChainD);
|
||||||
|
|
||||||
|
// Keep strong reference to command buffer
|
||||||
|
id<MTLCommandBuffer> commandBuffer = swapChainD->cbWrapper.d->cb;
|
||||||
|
|
||||||
__block int thisFrameSlot = currentFrameSlot;
|
__block int thisFrameSlot = currentFrameSlot;
|
||||||
[swapChainD->cbWrapper.d->cb addCompletedHandler: ^(id<MTLCommandBuffer> cb) {
|
[commandBuffer addCompletedHandler: ^(id<MTLCommandBuffer> cb) {
|
||||||
swapChainD->d->lastGpuTime[thisFrameSlot] += cb.GPUEndTime - cb.GPUStartTime;
|
swapChainD->d->lastGpuTime[thisFrameSlot] += cb.GPUEndTime - cb.GPUStartTime;
|
||||||
dispatch_semaphore_signal(swapChainD->d->sem[thisFrameSlot]);
|
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
|
// released before the command buffer is done with it. Manually keep it alive
|
||||||
// to work around this.
|
// to work around this.
|
||||||
id<MTLTexture> drawableTexture = [swapChainD->d->curDrawable.texture retain];
|
id<MTLTexture> drawableTexture = [swapChainD->d->curDrawable.texture retain];
|
||||||
[swapChainD->cbWrapper.d->cb addCompletedHandler:^(id<MTLCommandBuffer>) {
|
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>) {
|
||||||
[drawableTexture release];
|
[drawableTexture release];
|
||||||
}];
|
}];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const bool needsPresent = !flags.testFlag(QRhi::SkipPresent);
|
if (flags.testFlag(QRhi::SkipPresent)) {
|
||||||
const bool presentsWithTransaction = swapChainD->d->layer.presentsWithTransaction;
|
// Just need to commit, that's it
|
||||||
if (!presentsWithTransaction && needsPresent) {
|
[commandBuffer commit];
|
||||||
// beginFrame-endFrame without a render pass inbetween means there is no drawable.
|
} else {
|
||||||
if (id<CAMetalDrawable> 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 (id<CAMetalDrawable> drawable = swapChainD->d->curDrawable) {
|
if (id<CAMetalDrawable> drawable = swapChainD->d->curDrawable) {
|
||||||
// The layer has presentsWithTransaction set to true to avoid flicker on resizing,
|
// Got something to present
|
||||||
// so here it is important to follow what the Metal docs say when it comes to the
|
if (swapChainD->d->layer.presentsWithTransaction) {
|
||||||
// issuing the present.
|
[commandBuffer commit];
|
||||||
[swapChainD->cbWrapper.d->cb waitUntilScheduled];
|
// Keep strong reference to Metal layer
|
||||||
[drawable present];
|
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<QMetalLayer*>(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<QMetalLayer*>(swapChainD->d->layer);
|
||||||
|
[commandBuffer addScheduledHandler:^(id<MTLCommandBuffer>) {
|
||||||
|
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
|
// 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];
|
[d->captureScope endScope];
|
||||||
|
|
||||||
if (needsPresent)
|
|
||||||
swapChainD->currentFrameSlot = (swapChainD->currentFrameSlot + 1) % QMTL_FRAMES_IN_FLIGHT;
|
|
||||||
|
|
||||||
swapChainD->frameCount += 1;
|
swapChainD->frameCount += 1;
|
||||||
currentSwapChain = nullptr;
|
currentSwapChain = nullptr;
|
||||||
return QRhi::FrameOpSuccess;
|
return QRhi::FrameOpSuccess;
|
||||||
@ -6168,12 +6210,6 @@ void QMetalSwapChain::destroy()
|
|||||||
d->msaaTex[i] = nil;
|
d->msaaTex[i] = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
|
||||||
d->liveResizeStartObserver.remove();
|
|
||||||
d->liveResizeEndObserver.remove();
|
|
||||||
d->liveResizeObserverSet = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
d->layer = nullptr;
|
d->layer = nullptr;
|
||||||
m_proxyData = {};
|
m_proxyData = {};
|
||||||
|
|
||||||
@ -6380,34 +6416,6 @@ bool QMetalSwapChain::createOrResize()
|
|||||||
|
|
||||||
[d->layer setDevice: rhiD->d->dev];
|
[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<NSView *>(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 release];
|
||||||
d->curDrawable = nil;
|
d->curDrawable = nil;
|
||||||
|
|
||||||
|
@ -471,25 +471,6 @@ void QCocoaScreen::deliverUpdateRequests()
|
|||||||
if (!platformWindow->updatesWithDisplayLink())
|
if (!platformWindow->updatesWithDisplayLink())
|
||||||
continue;
|
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();
|
platformWindow->deliverUpdateRequest();
|
||||||
|
|
||||||
// Another update request was triggered, keep the display link running
|
// Another update request was triggered, keep the display link running
|
||||||
|
@ -124,6 +124,7 @@ public:
|
|||||||
|
|
||||||
Q_NOTIFICATION_HANDLER(NSWindowDidMoveNotification) void windowDidMove();
|
Q_NOTIFICATION_HANDLER(NSWindowDidMoveNotification) void windowDidMove();
|
||||||
Q_NOTIFICATION_HANDLER(NSWindowDidResizeNotification) void windowDidResize();
|
Q_NOTIFICATION_HANDLER(NSWindowDidResizeNotification) void windowDidResize();
|
||||||
|
Q_NOTIFICATION_HANDLER(NSWindowWillStartLiveResizeNotification) void windowWillStartLiveResize();
|
||||||
Q_NOTIFICATION_HANDLER(NSWindowDidEndLiveResizeNotification) void windowDidEndLiveResize();
|
Q_NOTIFICATION_HANDLER(NSWindowDidEndLiveResizeNotification) void windowDidEndLiveResize();
|
||||||
Q_NOTIFICATION_HANDLER(NSWindowDidBecomeKeyNotification) void windowDidBecomeKey();
|
Q_NOTIFICATION_HANDLER(NSWindowDidBecomeKeyNotification) void windowDidBecomeKey();
|
||||||
Q_NOTIFICATION_HANDLER(NSWindowDidResignKeyNotification) void windowDidResignKey();
|
Q_NOTIFICATION_HANDLER(NSWindowDidResignKeyNotification) void windowDidResignKey();
|
||||||
@ -188,6 +189,8 @@ public:
|
|||||||
Q_DECLARE_FLAGS(RecreationReasons, RecreationReason)
|
Q_DECLARE_FLAGS(RecreationReasons, RecreationReason)
|
||||||
Q_FLAG(RecreationReasons)
|
Q_FLAG(RecreationReasons)
|
||||||
|
|
||||||
|
bool inLiveResize() const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void recreateWindowIfNeeded();
|
void recreateWindowIfNeeded();
|
||||||
QCocoaNSWindow *createNSWindow(bool shouldBePanel);
|
QCocoaNSWindow *createNSWindow(bool shouldBePanel);
|
||||||
@ -234,6 +237,7 @@ public: // for QNSView
|
|||||||
bool m_inSetVisible = false;
|
bool m_inSetVisible = false;
|
||||||
bool m_inSetGeometry = false;
|
bool m_inSetGeometry = false;
|
||||||
bool m_inSetStyleMask = false;
|
bool m_inSetStyleMask = false;
|
||||||
|
bool m_inLiveResize = false;
|
||||||
|
|
||||||
QCocoaMenuBar *m_menubar = nullptr;
|
QCocoaMenuBar *m_menubar = nullptr;
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
#include <qpa/qplatformscreen.h>
|
#include <qpa/qplatformscreen.h>
|
||||||
#include <QtGui/private/qcoregraphics_p.h>
|
#include <QtGui/private/qcoregraphics_p.h>
|
||||||
#include <QtGui/private/qhighdpiscaling_p.h>
|
#include <QtGui/private/qhighdpiscaling_p.h>
|
||||||
|
#include <QtGui/private/qmetallayer_p.h>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
@ -1284,8 +1285,26 @@ void QCocoaWindow::windowDidResize()
|
|||||||
handleWindowStateChanged();
|
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()
|
void QCocoaWindow::windowDidEndLiveResize()
|
||||||
{
|
{
|
||||||
|
m_inLiveResize = false;
|
||||||
|
|
||||||
if (!isContentView())
|
if (!isContentView())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -1671,6 +1690,23 @@ bool QCocoaWindow::updatesWithDisplayLink() const
|
|||||||
void QCocoaWindow::deliverUpdateRequest()
|
void QCocoaWindow::deliverUpdateRequest()
|
||||||
{
|
{
|
||||||
qCDebug(lcQpaDrawing) << "Delivering update request to" << window();
|
qCDebug(lcQpaDrawing) << "Delivering update request to" << window();
|
||||||
|
|
||||||
|
if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(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();
|
QPlatformWindow::deliverUpdateRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,9 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "qcocoaintegration.h"
|
#include "qcocoaintegration.h"
|
||||||
#include <QtGui/private/qmacmimeregistry_p.h>
|
#include <QtGui/private/qmacmimeregistry_p.h>
|
||||||
|
#include <QtGui/private/qmetallayer_p.h>
|
||||||
|
|
||||||
|
#include <QuartzCore/CATransaction.h>
|
||||||
|
|
||||||
@interface QNSView (Drawing) <CALayerDelegate>
|
@interface QNSView (Drawing) <CALayerDelegate>
|
||||||
- (void)initDrawing;
|
- (void)initDrawing;
|
||||||
|
@ -75,7 +75,10 @@
|
|||||||
// too late at this point and the QWindow will be non-functional,
|
// too late at this point and the QWindow will be non-functional,
|
||||||
// but we can at least print a warning.
|
// but we can at least print a warning.
|
||||||
if ([MTLCreateSystemDefaultDevice() autorelease]) {
|
if ([MTLCreateSystemDefaultDevice() autorelease]) {
|
||||||
return [CAMetalLayer layer];
|
static bool allowPresentsWithTransaction =
|
||||||
|
!qEnvironmentVariableIsSet("QT_MTL_NO_TRANSACTION");
|
||||||
|
return allowPresentsWithTransaction ?
|
||||||
|
[QMetalLayer layer] : [CAMetalLayer layer];
|
||||||
} else {
|
} else {
|
||||||
qCWarning(lcQpaDrawing) << "Failed to create QWindow::MetalSurface."
|
qCWarning(lcQpaDrawing) << "Failed to create QWindow::MetalSurface."
|
||||||
<< "Metal is not supported by any of the GPUs in this system.";
|
<< "Metal is not supported by any of the GPUs in this system.";
|
||||||
@ -222,8 +225,39 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window();
|
const auto handleExposeEvent = [&]{
|
||||||
m_platformWindow->handleExposeEvent(QRectF::fromCGRect(self.bounds).toRect());
|
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<QMetalLayer*>(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
|
@end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user