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>bb10
parent
b3dc96ef4c
commit
85472b6b02
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue