From 85472b6b02b42ea624e1c00a5fd38c0d2889a731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Mon, 2 Jul 2018 23:10:13 +0200 Subject: macOS: Deliver update request via CVDisplayLink if swapInterval > 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We use GCD for marshaling the display link update over to the main thread, as Qt requires that the update request is delivered there. Change-Id: I318a5b8f27dc5094ce71244401308a4044c41b39 Reviewed-by: Morten Johan Sørvig --- src/plugins/platforms/cocoa/cocoa.pro | 2 +- src/plugins/platforms/cocoa/qcocoascreen.h | 8 ++ src/plugins/platforms/cocoa/qcocoascreen.mm | 204 ++++++++++++++++++++++++++++ src/plugins/platforms/cocoa/qcocoawindow.mm | 18 ++- 4 files changed, 228 insertions(+), 4 deletions(-) (limited to 'src/plugins/platforms/cocoa') diff --git a/src/plugins/platforms/cocoa/cocoa.pro b/src/plugins/platforms/cocoa/cocoa.pro index fd6dae8107..95a26a433c 100644 --- a/src/plugins/platforms/cocoa/cocoa.pro +++ b/src/plugins/platforms/cocoa/cocoa.pro @@ -82,7 +82,7 @@ qtConfig(vulkan) { RESOURCES += qcocoaresources.qrc -LIBS += -framework AppKit -framework CoreServices -framework Carbon -framework IOKit -framework QuartzCore -framework Metal -lcups +LIBS += -framework AppKit -framework CoreServices -framework Carbon -framework IOKit -framework QuartzCore -framework CoreVideo -framework Metal -lcups QT += \ core-private gui-private \ diff --git a/src/plugins/platforms/cocoa/qcocoascreen.h b/src/plugins/platforms/cocoa/qcocoascreen.h index 850ccaad7a..afb294240e 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.h +++ b/src/plugins/platforms/cocoa/qcocoascreen.h @@ -77,6 +77,10 @@ public: NSScreen *nativeScreen() const; void updateGeometry(); + void requestUpdate(); + void deliverUpdateRequests(); + bool isRunningDisplayLink() const; + static QCocoaScreen *primaryScreen(); static CGPoint mapToNative(const QPointF &pos, QCocoaScreen *screen = QCocoaScreen::primaryScreen()); @@ -96,6 +100,10 @@ public: QSizeF m_physicalSize; QCocoaCursor *m_cursor; QList m_siblings; + + CVDisplayLinkRef m_displayLink = nullptr; + dispatch_source_t m_displayLinkSource = nullptr; + QAtomicInt m_pendingUpdates; }; #ifndef QT_NO_DEBUG_STREAM diff --git a/src/plugins/platforms/cocoa/qcocoascreen.mm b/src/plugins/platforms/cocoa/qcocoascreen.mm index d639b56064..80fe83cc5b 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -47,6 +47,10 @@ #include +#include + +#include + QT_BEGIN_NAMESPACE class QCoreTextFontEngine; @@ -62,6 +66,10 @@ QCocoaScreen::QCocoaScreen(int screenIndex) QCocoaScreen::~QCocoaScreen() { delete m_cursor; + + CVDisplayLinkRelease(m_displayLink); + if (m_displayLinkSource) + dispatch_release(m_displayLinkSource); } NSScreen *QCocoaScreen::nativeScreen() const @@ -139,6 +147,202 @@ void QCocoaScreen::updateGeometry() QWindowSystemInterface::handleScreenRefreshRateChange(screen(), m_refreshRate); } +// ----------------------- Display link ----------------------- + +Q_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg); + +void QCocoaScreen::requestUpdate() +{ + if (!m_displayLink) { + CVDisplayLinkCreateWithCGDisplay(nativeScreen().qt_displayId, &m_displayLink); + CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*, + const CVTimeStamp*, CVOptionFlags, CVOptionFlags*, void* displayLinkContext) -> int { + // FIXME: It would be nice if update requests would include timing info + static_cast(displayLinkContext)->deliverUpdateRequests(); + return kCVReturnSuccess; + }, this); + qCDebug(lcQpaScreenUpdates) << "Display link created for" << this; + + // During live window resizing -[NSWindow _resizeWithEvent:] will spin a local event loop + // in event-tracking mode, dequeuing only the mouse drag events needed to update the window's + // frame. It will repeatedly spin this loop until no longer receiving any mouse drag events, + // and will then update the frame (effectively coalescing/compressing the events). Unfortunately + // the events are pulled out using -[NSApplication nextEventMatchingEventMask:untilDate:inMode:dequeue:] + // which internally uses CFRunLoopRunSpecific, so the event loop will also process GCD queues and other + // runloop sources that have been added to the tracking mode. This includes the GCD display-link + // source that we use to marshal the display-link callback over to the main thread. If the + // subsequent delivery of the update-request on the main thread stalls due to inefficient + // user code, the NSEventThread will have had time to deliver additional mouse drag events, + // and the logic in -[NSWindow _resizeWithEvent:] will keep on compressing events and never + // get to the point of actually updating the window frame, making it seem like the window + // is stuck in its original size. Only when the user stops moving their mouse, and the event + // queue is completely drained of drag events, will the window frame be updated. + + // By keeping an event tap listening for drag events, registered as a version 1 runloop source, + // we prevent the GCD source from being prioritized, giving the resize logic enough time + // to finish coalescing the events. This is incidental, but conveniently gives us the behavior + // we are looking for, interleaving display-link updates and resize events. + static CFMachPortRef eventTap = []() { + CFMachPortRef eventTap = CGEventTapCreateForPid(getpid(), kCGTailAppendEventTap, + kCGEventTapOptionListenOnly, NSEventMaskLeftMouseDragged, + [](CGEventTapProxy, CGEventType type, CGEventRef event, void *) -> CGEventRef { + if (type == kCGEventTapDisabledByTimeout) + qCWarning(lcQpaScreenUpdates) << "Event tap disabled due to timeout!"; + return event; // Listen only tap, so what we return doesn't really matter + }, nullptr); + CGEventTapEnable(eventTap, false); // Event taps are normally enabled when created + static CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserverForName:NSWindowWillStartLiveResizeNotification object:nil queue:nil + usingBlock:^(NSNotification *notification) { + qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object + << "started. Enabling event tap"; + CGEventTapEnable(eventTap, true); + }]; + [center addObserverForName:NSWindowDidEndLiveResizeNotification object:nil queue:nil + usingBlock:^(NSNotification *notification) { + qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object + << "ended. Disabling event tap"; + CGEventTapEnable(eventTap, false); + }]; + return eventTap; + }(); + Q_UNUSED(eventTap); + } + + if (!CVDisplayLinkIsRunning(m_displayLink)) { + qCDebug(lcQpaScreenUpdates) << "Starting display link for" << this; + CVDisplayLinkStart(m_displayLink); + } +} + +// Helper to allow building up debug output in multiple steps +struct DeferredDebugHelper +{ + DeferredDebugHelper(const QLoggingCategory &cat) { + if (cat.isDebugEnabled()) + debug = new QDebug(QMessageLogger().debug(cat).nospace()); + } + ~DeferredDebugHelper() { + flushOutput(); + } + void flushOutput() { + if (debug) { + delete debug; + debug = nullptr; + } + } + QDebug *debug = nullptr; +}; + +#define qDeferredDebug(helper) if (Q_UNLIKELY(helper.debug)) *helper.debug + +void QCocoaScreen::deliverUpdateRequests() +{ + if (!QGuiApplication::instance()) + return; + + // The CVDisplayLink callback is a notification that it's a good time to produce a new frame. + // Since the callback is delivered on a separate thread we have to marshal it over to the + // main thread, as Qt requires update requests to be delivered there. This needs to happen + // asynchronously, as otherwise we may end up deadlocking if the main thread calls back + // into any of the CVDisplayLink APIs. + if (QThread::currentThread() != QGuiApplication::instance()->thread()) { + // We're explicitly not using the data of the GCD source to track the pending updates, + // as the data isn't reset to 0 until after the event handler, and also doesn't update + // during the event handler, both of which we need to track late frames. + const int pendingUpdates = ++m_pendingUpdates; + + DeferredDebugHelper screenUpdates(lcQpaScreenUpdates()); + qDeferredDebug(screenUpdates) << "display link callback for screen " << m_screenIndex; + + if (const int framesAheadOfDelivery = pendingUpdates - 1) { + // If we have more than one update pending it means that a previous display link callback + // has not been fully processed on the main thread, either because GCD hasn't delivered + // it on the main thread yet, because the processing of the update request is taking + // too long, or because the update request was deferred due to window live resizing. + qDeferredDebug(screenUpdates) << ", " << framesAheadOfDelivery << " frame(s) ahead"; + + // We skip the frame completely if we're live-resizing, to not put any extra + // strain on the main thread runloop. Otherwise we assume we should push frames + // as fast as possible, and hopefully the callback will be delivered on the + // main thread just when the previous finished. + if (qt_apple_sharedApplication().keyWindow.inLiveResize) { + qDeferredDebug(screenUpdates) << "; waiting for main thread to catch up"; + return; + } + } + + qDeferredDebug(screenUpdates) << "; signaling dispatch source"; + + if (!m_displayLinkSource) { + m_displayLinkSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_event_handler(m_displayLinkSource, ^{ + deliverUpdateRequests(); + }); + dispatch_resume(m_displayLinkSource); + } + + dispatch_source_merge_data(m_displayLinkSource, 1); + + } else { + DeferredDebugHelper screenUpdates(lcQpaScreenUpdates()); + qDeferredDebug(screenUpdates) << "gcd event handler on main thread"; + + const int pendingUpdates = m_pendingUpdates; + if (pendingUpdates > 1) + qDeferredDebug(screenUpdates) << ", " << (pendingUpdates - 1) << " frame(s) behind display link"; + + screenUpdates.flushOutput(); + + bool pauseUpdates = true; + + auto windows = QGuiApplication::allWindows(); + for (int i = 0; i < windows.size(); ++i) { + QWindow *window = windows.at(i); + QPlatformWindow *platformWindow = window->handle(); + if (!platformWindow) + continue; + + if (!platformWindow->hasPendingUpdateRequest()) + continue; + + if (window->screen() != screen()) + continue; + + // Skip windows that are not doing update requests via display link + if (!(window->format().swapInterval() > 0)) + continue; + + platformWindow->deliverUpdateRequest(); + + // Another update request was triggered, keep the display link running + if (platformWindow->hasPendingUpdateRequest()) + pauseUpdates = false; + } + + if (pauseUpdates) { + // Pause the display link if there are no pending update requests + qCDebug(lcQpaScreenUpdates) << "Stopping display link for" << this; + CVDisplayLinkStop(m_displayLink); + } + + if (const int missedUpdates = m_pendingUpdates.fetchAndStoreRelaxed(0) - pendingUpdates) { + qCWarning(lcQpaScreenUpdates) << "main thread missed" << missedUpdates + << "update(s) from display link during update request delivery"; + } + } +} + +bool QCocoaScreen::isRunningDisplayLink() const +{ + return m_displayLink && CVDisplayLinkIsRunning(m_displayLink); +} + +// ----------------------------------------------------------- + qreal QCocoaScreen::devicePixelRatio() const { QMacAutoReleasePool pool; diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 5702d01cb3..47e13a9e8c 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -1070,8 +1070,12 @@ void QCocoaWindow::windowDidChangeScreen() if (!window()) return; - if (QCocoaScreen *cocoaScreen = QCocoaIntegration::instance()->screenForNSScreen(m_view.window.screen)) + if (QCocoaScreen *cocoaScreen = QCocoaIntegration::instance()->screenForNSScreen(m_view.window.screen)) { QWindowSystemInterface::handleWindowScreenChanged(window(), cocoaScreen->screen()); + + if (hasPendingUpdateRequest() && cocoaScreen->isRunningDisplayLink()) + requestUpdate(); // Restart display-link on new screen + } } void QCocoaWindow::windowWillClose() @@ -1330,8 +1334,16 @@ void QCocoaWindow::recreateWindowIfNeeded() void QCocoaWindow::requestUpdate() { - qCDebug(lcQpaDrawing) << "QCocoaWindow::requestUpdate" << window(); - QPlatformWindow::requestUpdate(); + const int swapInterval = format().swapInterval(); + qCDebug(lcQpaDrawing) << "QCocoaWindow::requestUpdate" << window() << "swapInterval" << swapInterval; + + if (swapInterval > 0) { + // Vsync is enabled, deliver via CVDisplayLink + static_cast(screen())->requestUpdate(); + } else { + // Fall back to the un-throttled timer-based callback + QPlatformWindow::requestUpdate(); + } } void QCocoaWindow::deliverUpdateRequest() -- cgit v1.2.3