rhi: metal: macOS: Set presentsWithTransaction during resize

...when presenting from the main thread.

Make resizing nice and smooth in application that render and present
on the main thread. This includes Qt Quick applications when ran
with QSG_RENDER_LOOP=basic, as well as QQuickWidget-based widget apps
and anything using the QRhi-based backingstore flushing, and plain
QWindow apps that use QRhi on the main thread.

Allow opting out with an environment variable (QT_MTL_NO_TRANSACTION),
following the existing patterns of some of the other backends (such as
QT_D3D_NO_xxxx). This can be handy in case it turns out setting
presentsWithTransaction is not desired in some very specific case.

Task-number: QTBUG-107198
Change-Id: Id0f4e5a509076dd24c03d243c4f098ddb139d7af
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Laszlo Agocs 2023-03-10 17:02:48 +01:00
parent fb70893dd5
commit 401edea982

View File

@ -417,6 +417,11 @@ struct QMetalSwapChainData
id<MTLTexture> 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)
@ -2291,26 +2296,29 @@ QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrame
QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, swapChain);
Q_ASSERT(currentSwapChain == swapChainD);
__block int thisFrameSlot = currentFrameSlot;
[swapChainD->cbWrapper.d->cb addCompletedHandler: ^(id<MTLCommandBuffer>) {
dispatch_semaphore_signal(swapChainD->d->sem[thisFrameSlot]);
}];
const bool needsPresent = !flags.testFlag(QRhi::SkipPresent);
if (needsPresent) {
// beginFrame-endFrame without a render pass inbetween means there is no
// drawable, handle this gracefully because presentDrawable does not like
// null arguments.
if (id<CAMetalDrawable> drawable = swapChainD->d->curDrawable) {
// QTBUG-103415: while the docs suggest the following two approaches are
// equivalent, there is a difference in case a frame is recorded earlier than
// (i.e. not in response to) the next CVDisplayLink callback. Therefore, stick
// with presentDrawable, which gives results identical to OpenGL, and all other
// platforms, i.e. throttles to vsync as expected, meaning constant 15-17 ms with
// a 60 Hz screen, no jumps with smaller intervals, regardless of when the frame
// is submitted by the app)
#if 1
const bool presentsWithTransaction = swapChainD->d->layer.presentsWithTransaction;
if (!presentsWithTransaction && needsPresent) {
// beginFrame-endFrame without a render pass inbetween means there is no drawable.
if (id<CAMetalDrawable> drawable = swapChainD->d->curDrawable)
[swapChainD->cbWrapper.d->cb presentDrawable: drawable];
#else
[swapChainD->cbWrapper.d->cb addScheduledHandler:^(id<MTLCommandBuffer>) {
[drawable present];
}];
#endif
}
[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) {
// 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];
}
}
@ -2318,13 +2326,6 @@ QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrame
[swapChainD->d->curDrawable release];
swapChainD->d->curDrawable = nil;
__block int thisFrameSlot = currentFrameSlot;
[swapChainD->cbWrapper.d->cb addCompletedHandler: ^(id<MTLCommandBuffer>) {
dispatch_semaphore_signal(swapChainD->d->sem[thisFrameSlot]);
}];
[swapChainD->cbWrapper.d->cb commit];
[d->captureScope endScope];
if (needsPresent)
@ -5974,6 +5975,12 @@ void QMetalSwapChain::destroy()
d->msaaTex[i] = nil;
}
#ifdef Q_OS_MACOS
d->liveResizeStartObserver.remove();
d->liveResizeEndObserver.remove();
d->liveResizeObserverSet = false;
#endif
d->layer = nullptr;
[d->curDrawable release];
@ -6163,6 +6170,34 @@ 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<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 = nil;