macOS: Stop relying on balanced CGDisplay reconfiguration callbacks

We were using CGDisplay callbacks to determine when a screen reconfiguration
had happened, and when it had propagated to changes in NSScreen.screens,
so that we could update our QScreen view of the world.

Unfortunately the CGDisplay callbacks were not deterministic enough to
use as a signal for when a reconfigure had completed.

Since we can't rely on NSApplicationDidChangeScreenParametersNotification
either (it comes in too late), we're now resorting to updating our QScreens
at every chance we get:

 - On every CGDisplay reconfiguration ending
 - On QCocoaWindow::windowDidChangeScreen() as a result of AppKit
   moving the window.
 - On NSApplicationDidChangeScreenParametersNotification
 - On QCocoaScreen::get() as a last resort

Since the result of these updates are only reflected as QScreen property
updates or QGuiApplication signals if a change actually occurred, it should
be safe to update early and often.

Task-number: QTBUG-77656
Fixes: QTBUG-80193
Change-Id: I98334a66767736d94ad2fcb169e65f0d8bc71a30
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
(cherry picked from commit 6e250179229ebe7e2a056ba0e363592f4d1f6972)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Tor Arne Vestbø 2021-06-25 13:02:02 +02:00 committed by Qt Cherry-pick Bot
parent 89872d0852
commit a609761b71
3 changed files with 31 additions and 85 deletions

View File

@ -106,9 +106,6 @@ private:
static QMacNotificationObserver s_screenParameterObserver; static QMacNotificationObserver s_screenParameterObserver;
static CGDisplayReconfigurationCallBack s_displayReconfigurationCallBack; static CGDisplayReconfigurationCallBack s_displayReconfigurationCallBack;
static bool updateScreensIfNeeded();
static NSArray *s_screenConfigurationBeforeUpdate;
static void add(CGDirectDisplayID displayId); static void add(CGDirectDisplayID displayId);
QCocoaScreen(CGDirectDisplayID displayId); QCocoaScreen(CGDirectDisplayID displayId);
void update(CGDirectDisplayID displayId); void update(CGDirectDisplayID displayId);

View File

@ -74,7 +74,6 @@ namespace CoreGraphics {
Q_ENUM_NS(DisplayChange) Q_ENUM_NS(DisplayChange)
} }
NSArray *QCocoaScreen::s_screenConfigurationBeforeUpdate = nil;
QMacNotificationObserver QCocoaScreen::s_screenParameterObserver; QMacNotificationObserver QCocoaScreen::s_screenParameterObserver;
CGDisplayReconfigurationCallBack QCocoaScreen::s_displayReconfigurationCallBack = nullptr; CGDisplayReconfigurationCallBack QCocoaScreen::s_displayReconfigurationCallBack = nullptr;
@ -85,83 +84,23 @@ void QCocoaScreen::initializeScreens()
s_displayReconfigurationCallBack = [](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) { s_displayReconfigurationCallBack = [](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) {
Q_UNUSED(userInfo); Q_UNUSED(userInfo);
// Displays are reconfigured in batches, and we want to update our screens
// once a batch ends, so that all the states of the displays are up to date.
static int displayReconfigurationsInProgress = 0;
const bool beforeReconfigure = flags & kCGDisplayBeginConfigurationFlag; const bool beforeReconfigure = flags & kCGDisplayBeginConfigurationFlag;
qCDebug(lcQpaScreen).verbosity(0).nospace() << "Display " << displayId qCDebug(lcQpaScreen).verbosity(0) << "Display" << displayId
<< (beforeReconfigure ? " about to reconfigure" : " was ") << (beforeReconfigure ? "beginning" : "finished") << "reconfigure"
<< QFlags<CoreGraphics::DisplayChange>(flags) << QFlags<CoreGraphics::DisplayChange>(flags);
<< " with " << displayReconfigurationsInProgress
<< " display configuration(s) in progress";
if (!flags) { if (!beforeReconfigure)
// CGDisplayRegisterReconfigurationCallback has been observed to be called updateScreens();
// with flags unset. This seems like a bug. The callback is not paired with
// a matching "completion" callback either, so we don't know whether to treat
// it as a begin or end of reconfigure.
return;
}
if (beforeReconfigure) {
if (!displayReconfigurationsInProgress++) {
// There might have been a screen reconfigure before this that
// we didn't process yet, so do that now if that's the case.
updateScreensIfNeeded();
Q_ASSERT(!s_screenConfigurationBeforeUpdate);
s_screenConfigurationBeforeUpdate = NSScreen.screens;
qCDebug(lcQpaScreen, "Display reconfigure transaction started"
" with screen configuration %p", s_screenConfigurationBeforeUpdate);
static void (^tryScreenUpdate)();
tryScreenUpdate = ^void () {
qCDebug(lcQpaScreen) << "Attempting screen update from runloop block";
if (!updateScreensIfNeeded())
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, tryScreenUpdate);
};
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, tryScreenUpdate);
}
} else {
Q_ASSERT_X(displayReconfigurationsInProgress, "QCococaScreen",
"Display configuration transactions are expected to be balanced");
if (!--displayReconfigurationsInProgress) {
qCDebug(lcQpaScreen) << "Display reconfigure transaction completed";
// We optimistically update now, in case the NSScreens have changed
updateScreensIfNeeded();
}
}
}; };
CGDisplayRegisterReconfigurationCallback(s_displayReconfigurationCallBack, nullptr); CGDisplayRegisterReconfigurationCallback(s_displayReconfigurationCallBack, nullptr);
s_screenParameterObserver = QMacNotificationObserver(NSApplication.sharedApplication, s_screenParameterObserver = QMacNotificationObserver(NSApplication.sharedApplication,
NSApplicationDidChangeScreenParametersNotification, [&]() { NSApplicationDidChangeScreenParametersNotification, [&]() {
qCDebug(lcQpaScreen) << "Received screen parameter change notification"; qCDebug(lcQpaScreen) << "Received screen parameter change notification";
updateScreensIfNeeded(); // As a last resort we update screens here updateScreens();
}); });
} }
bool QCocoaScreen::updateScreensIfNeeded()
{
if (!s_screenConfigurationBeforeUpdate) {
qCDebug(lcQpaScreen) << "QScreens have already been updated, all good";
return true;
}
if (s_screenConfigurationBeforeUpdate == NSScreen.screens) {
qCDebug(lcQpaScreen) << "Still waiting for NSScreen configuration change";
return false;
}
qCDebug(lcQpaScreen, "NSScreen configuration changed to %p", NSScreen.screens);
updateScreens();
s_screenConfigurationBeforeUpdate = nil;
return true;
}
/* /*
Update the list of available QScreens, and the properties of existing screens. Update the list of available QScreens, and the properties of existing screens.
@ -321,15 +260,18 @@ void QCocoaScreen::update(CGDirectDisplayID displayId)
Q_ASSERT(isOnline()); Q_ASSERT(isOnline());
// Some properties are only available via NSScreen
NSScreen *nsScreen = nativeScreen();
if (!nsScreen) {
qCDebug(lcQpaScreen) << "Corresponding NSScreen not yet available. Deferring update";
return;
}
const QRect previousGeometry = m_geometry; const QRect previousGeometry = m_geometry;
const QRect previousAvailableGeometry = m_availableGeometry; const QRect previousAvailableGeometry = m_availableGeometry;
const QDpi previousLogicalDpi = m_logicalDpi; const QDpi previousLogicalDpi = m_logicalDpi;
const qreal previousRefreshRate = m_refreshRate; const qreal previousRefreshRate = m_refreshRate;
// Some properties are only available via NSScreen
NSScreen *nsScreen = nativeScreen();
Q_ASSERT(nsScreen);
// The reference screen for the geometry is always the primary screen // The reference screen for the geometry is always the primary screen
QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID())); QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID()));
m_geometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.frame), primaryScreenGeometry).toRect(); m_geometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.frame), primaryScreenGeometry).toRect();
@ -759,13 +701,17 @@ QList<QPlatformScreen*> QCocoaScreen::virtualSiblings() const
QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen) QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen)
{ {
if (s_screenConfigurationBeforeUpdate) { auto displayId = nsScreen.qt_displayId;
qCWarning(lcQpaScreen) << "Trying to resolve screen while waiting for screen reconfigure!"; auto *cocoaScreen = get(displayId);
if (!updateScreensIfNeeded()) if (!cocoaScreen) {
qCWarning(lcQpaScreen) << "Failed to do last minute screen update. Expect crashes."; qCWarning(lcQpaScreen) << "Failed to map" << nsScreen
<< "to QCocoaScreen. Doing last minute update.";
updateScreens();
cocoaScreen = get(displayId);
if (!cocoaScreen)
qCWarning(lcQpaScreen) << "Last minute update failed!";
} }
return cocoaScreen;
return get(nsScreen.qt_displayId);
} }
QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId) QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId)

View File

@ -1268,11 +1268,15 @@ void QCocoaWindow::windowDidChangeScreen()
return; return;
// Note: When a window is resized to 0x0 Cocoa will report the window's screen as nil // Note: When a window is resized to 0x0 Cocoa will report the window's screen as nil
auto *currentScreen = QCocoaScreen::get(m_view.window.screen); NSScreen *nsScreen = m_view.window.screen;
auto *previousScreen = static_cast<QCocoaScreen*>(screen());
Q_ASSERT_X(!m_view.window.screen || currentScreen, qCDebug(lcQpaWindow) << window() << "did change" << nsScreen;
"QCocoaWindow", "Failed to get QCocoaScreen for NSScreen"); QCocoaScreen::updateScreens();
auto *previousScreen = static_cast<QCocoaScreen*>(screen());
auto *currentScreen = QCocoaScreen::get(nsScreen);
qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen;
// Note: The previous screen may be the same as the current screen, either because // Note: The previous screen may be the same as the current screen, either because
// a) the screen was just reconfigured, which still results in AppKit sending an // a) the screen was just reconfigured, which still results in AppKit sending an
@ -1285,7 +1289,6 @@ void QCocoaWindow::windowDidChangeScreen()
// device-pixel ratio may have changed, and needs to be delivered to all // device-pixel ratio may have changed, and needs to be delivered to all
// windows, both top level and child windows. // windows, both top level and child windows.
qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen;
QWindowSystemInterface::handleWindowScreenChanged<QWindowSystemInterface::SynchronousDelivery>( QWindowSystemInterface::handleWindowScreenChanged<QWindowSystemInterface::SynchronousDelivery>(
window(), currentScreen ? currentScreen->screen() : nullptr); window(), currentScreen ? currentScreen->screen() : nullptr);