summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Arne Vestbø <tor.arne.vestbo@qt.io>2018-07-02 23:10:13 +0200
committerTor Arne Vestbø <tor.arne.vestbo@qt.io>2018-07-07 14:40:57 +0000
commit85472b6b02b42ea624e1c00a5fd38c0d2889a731 (patch)
treea968d6b1e7df234dad747faee36390880656e3bc
parentb3dc96ef4c0d6387bed819e614b20285b48bcfb7 (diff)
macOS: Deliver update request via CVDisplayLink if swapInterval > 0
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 <morten.sorvig@qt.io>
-rw-r--r--src/plugins/platforms/cocoa/cocoa.pro2
-rw-r--r--src/plugins/platforms/cocoa/qcocoascreen.h8
-rw-r--r--src/plugins/platforms/cocoa/qcocoascreen.mm204
-rw-r--r--src/plugins/platforms/cocoa/qcocoawindow.mm18
4 files changed, 228 insertions, 4 deletions
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<QPlatformScreen *> 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 <IOKit/graphics/IOGraphicsLib.h>
+#include <QtGui/private/qwindow_p.h>
+
+#include <QtCore/private/qeventdispatcher_cf_p.h>
+
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<QCocoaScreen*>(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<QWindowSystemInterface::SynchronousDelivery>(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<QCocoaScreen *>(screen())->requestUpdate();
+ } else {
+ // Fall back to the un-throttled timer-based callback
+ QPlatformWindow::requestUpdate();
+ }
}
void QCocoaWindow::deliverUpdateRequest()