Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qiosscreen.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses
6
7#include "qiosglobal.h"
9#include "qiosscreen.h"
10#include "qioswindow.h"
11#include <qpa/qwindowsysteminterface.h>
14#include "quiview.h"
15#include "qiostheme.h"
16#include "quiwindow.h"
17
18#include <QtCore/private/qcore_mac_p.h>
19
20#include <QtGui/qpointingdevice.h>
21#include <QtGui/private/qwindow_p.h>
22#include <QtGui/private/qguiapplication_p.h>
23#include <private/qcoregraphics_p.h>
24#include <qpa/qwindowsysteminterface.h>
25
26#include <Metal/Metal.h>
27
28#include <sys/sysctl.h>
29
30// -------------------------------------------------------------------------
31
32typedef void (^DisplayLinkBlock)(CADisplayLink *displayLink);
33
34@implementation UIScreen (DisplayLinkBlock)
35- (CADisplayLink*)displayLinkWithBlock:(DisplayLinkBlock)block
36{
37 return [self displayLinkWithTarget:[[block copy] autorelease]
38 selector:@selector(invokeDisplayLinkBlock:)];
39}
40@end
41
42@implementation NSObject (DisplayLinkBlock)
43- (void)invokeDisplayLinkBlock:(CADisplayLink *)sender
44{
45 DisplayLinkBlock block = static_cast<id>(self);
46 block(sender);
47}
48@end
49
50
51// -------------------------------------------------------------------------
52
53#if !defined(Q_OS_VISIONOS)
54static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen)
55{
56 foreach (QScreen *screen, QGuiApplication::screens()) {
57 QIOSScreen *platformScreen = static_cast<QIOSScreen *>(screen->handle());
58 if (platformScreen->uiScreen() == uiScreen)
59 return platformScreen;
60 }
61
62 return 0;
63}
64
65@interface QIOSScreenTracker : NSObject
66@end
67
68@implementation QIOSScreenTracker
69
70+ (void)load
71{
72 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
73 [center addObserver:self selector:@selector(screenConnected:)
74 name:UIScreenDidConnectNotification object:nil];
75 [center addObserver:self selector:@selector(screenDisconnected:)
76 name:UIScreenDidDisconnectNotification object:nil];
77 [center addObserver:self selector:@selector(screenModeChanged:)
78 name:UIScreenModeDidChangeNotification object:nil];
79}
80
81+ (void)screenConnected:(NSNotification*)notification
82{
83 if (!QIOSIntegration::instance())
84 return; // Will be added when QIOSIntegration is created
85
86 QWindowSystemInterface::handleScreenAdded(new QIOSScreen([notification object]));
87}
88
89+ (void)screenDisconnected:(NSNotification*)notification
90{
91 if (!QIOSIntegration::instance())
92 return;
93
94 QIOSScreen *screen = qtPlatformScreenFor([notification object]);
95 Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen disconnected that we didn't know about");
96
97 QWindowSystemInterface::handleScreenRemoved(screen);
98}
99
100+ (void)screenModeChanged:(NSNotification*)notification
101{
102 if (!QIOSIntegration::instance())
103 return;
104
105 QIOSScreen *screen = qtPlatformScreenFor([notification object]);
106 Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen changed that we didn't know about");
107
108 screen->updateProperties();
109}
110
111@end
112
113#endif // !defined(Q_OS_VISIONOS)
114
115// -------------------------------------------------------------------------
116
117QT_BEGIN_NAMESPACE
118
119using namespace Qt::StringLiterals;
120
121#if !defined(Q_OS_VISIONOS)
122/*!
123 Returns the model identifier of the device.
124*/
125static QString deviceModelIdentifier()
126{
127#if TARGET_OS_SIMULATOR
128 return QString::fromLocal8Bit(qgetenv("SIMULATOR_MODEL_IDENTIFIER"));
129#else
130 static const char key[] = "hw.machine";
131
132 size_t size;
133 sysctlbyname(key, NULL, &size, NULL, 0);
134
135 QVarLengthArray<char> value(size);
136 sysctlbyname(key, value.data(), &size, NULL, 0);
137
138 return QString::fromLatin1(QByteArrayView(value.constData(), qsizetype(size)));
139#endif
140}
141#endif // !defined(Q_OS_VISIONOS)
142
143
144void QIOSScreen::initializeScreens()
145{
146#if defined(Q_OS_VISIONOS)
147 // Qt requires a screen, so let's give it a dummy one
148 QWindowSystemInterface::handleScreenAdded(new QIOSScreen);
149#else
150 Q_ASSERT([UIScreen.screens containsObject:UIScreen.mainScreen]);
151 for (UIScreen *screen in UIScreen.screens)
152 QWindowSystemInterface::handleScreenAdded(new QIOSScreen(screen));
153#endif
154}
155
156#if defined(Q_OS_VISIONOS)
157QIOSScreen::QIOSScreen()
158{
159#else
160QIOSScreen::QIOSScreen(UIScreen *screen)
161 : m_uiScreen(screen)
162{
163 QString deviceIdentifier = deviceModelIdentifier();
164
165 if (screen == [UIScreen mainScreen] && !deviceIdentifier.startsWith("AppleTV")) {
166 // Based on https://en.wikipedia.org/wiki/List_of_iOS_devices#Display
167
168 // iPhone (1st gen), 3G, 3GS, and iPod Touch (1st–3rd gen) are 18-bit devices
169 static QRegularExpression lowBitDepthDevices("^(iPhone1,[12]|iPhone2,1|iPod[1-3],1)$");
170 m_depth = deviceIdentifier.contains(lowBitDepthDevices) ? 18 : 24;
171
172 static QRegularExpression iPhoneXModels("^iPhone(10,[36])$");
173 static QRegularExpression iPhonePlusModels("^iPhone(7,1|8,2|9,[24]|10,[25])$");
174 static QRegularExpression iPadMiniModels("^iPad(2,[567]|4,[4-9]|5,[12])$");
175
176 if (deviceIdentifier.contains(iPhoneXModels)) {
177 m_physicalDpi = 458;
178 } else if (deviceIdentifier.contains(iPhonePlusModels)) {
179 m_physicalDpi = 401;
180 } else if (deviceIdentifier.startsWith("iPad")) {
181 if (deviceIdentifier.contains(iPadMiniModels))
182 m_physicalDpi = 163 * devicePixelRatio();
183 else
184 m_physicalDpi = 132 * devicePixelRatio();
185 } else {
186 // All normal iPhones, and iPods
187 m_physicalDpi = 163 * devicePixelRatio();
188 }
189 } else {
190 // External display, hard to say
191 m_depth = 24;
192 m_physicalDpi = 96;
193 }
194
195 m_displayLink = [m_uiScreen displayLinkWithBlock:^(CADisplayLink *) { deliverUpdateRequests(); }];
196 m_displayLink.paused = YES; // Enabled when clients call QWindow::requestUpdate()
197 [m_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
198
199
200 // The screen brightness might affect the EDR headroom of the display,
201 // which might affect the rendering of windows that opt in to EDR.
202 m_screenBrightnessObserver = QMacNotificationObserver(m_uiScreen,
203 UIScreenBrightnessDidChangeNotification, [&]() {
204 for (auto *window : QPlatformScreen::windows()) {
205 auto *platformWindow = static_cast<QIOSWindow*>(window->handle());
206 if (!platformWindow)
207 continue;
208
209 UIView *view = platformWindow->view();
210
211 if (!view.layer.wantsExtendedDynamicRangeContent)
212 continue;
213
214 [view setNeedsDisplay];
215 }
216 });
217
218 if (shouldPauseDisplayLinkWhenInactive()) {
219 // We're pausing the display link if the application moves out of the active state,
220 // so make sure to deliver to any windows that need it once the app becomes active.
221 QObject::connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, [this](auto newState) {
222 if (newState == Qt::ApplicationActive) {
223 qCDebug(lcQpaApplication) << "Attempting update request delivery after becoming active";
224 deliverUpdateRequests();
225 }
226 });
227 }
228
229#endif // !defined(Q_OS_VISIONOS))
230
231 updateProperties();
232}
233
234QIOSScreen::~QIOSScreen()
235{
236 [m_displayLink invalidate];
237}
238
239QString QIOSScreen::name() const
240{
241#if defined(Q_OS_VISIONOS)
242 return {};
243#else
244 if (m_uiScreen == [UIScreen mainScreen])
245 return QString::fromNSString([UIDevice currentDevice].model) + " built-in display"_L1;
246 else
247 return "External display"_L1;
248#endif
249}
250
251void QIOSScreen::updateProperties()
252{
253 QRect previousGeometry = m_geometry;
254 QRect previousAvailableGeometry = m_availableGeometry;
255
256#if defined(Q_OS_VISIONOS)
257 // Based on what iPad app reports
258 m_geometry = QRectF::fromCGRect(rootViewForScreen(this).bounds).toRect();
259 m_depth = 24;
260#else
261 m_geometry = QRectF::fromCGRect(m_uiScreen.bounds).toRect();
262
263 if (m_geometry != previousGeometry) {
264 // We can't use the primaryOrientation of screen(), as we haven't reported the new geometry yet
265 Qt::ScreenOrientation primaryOrientation = m_geometry.width() >= m_geometry.height() ?
266 Qt::LandscapeOrientation : Qt::PortraitOrientation;
267
268 // On iPhone 6+ devices, or when display zoom is enabled, the render buffer is scaled
269 // before being output on the physical display. We have to take this into account when
270 // computing the physical size. Note that unlike the native bounds, the physical size
271 // follows the primary orientation of the screen.
272 const QRectF physicalGeometry = mapBetween(nativeOrientation(), primaryOrientation, QRectF::fromCGRect(m_uiScreen.nativeBounds).toRect());
273
274 static const qreal millimetersPerInch = 25.4;
275 m_physicalSize = physicalGeometry.size() / m_physicalDpi * millimetersPerInch;
276 }
277
278#endif // defined(Q_OS_VISIONOS)
279
280 // UIScreen does not provide a consistent accessor for the safe area margins
281 // of the screen, and on visionOS we won't even have a UIScreen, so we report
282 // the available geometry of the screen to be the same as the full geometry.
283 // Safe area margins and maximized state is handled in QIOSWindow::setWindowState.
284 m_availableGeometry = m_geometry;
285
286 // At construction time, we don't yet have an associated QScreen, but we still want
287 // to compute the properties above so they are ready for when the QScreen attaches.
288 // Also, at destruction time the QScreen has already been torn down, so notifying
289 // Qt about changes to the screen will cause asserts in the event delivery system.
290 if (!screen())
291 return;
292
293 if (screen()->orientation() != orientation())
294 QWindowSystemInterface::handleScreenOrientationChange(screen(), orientation());
295
296 // Note: The screen orientation change and the geometry changes are not atomic, so when
297 // the former is emitted, the latter has not been reported and reflected in the QScreen
298 // API yet. But conceptually it makes sense that the orientation update happens first,
299 // and the geometry updates caused by auto-rotation happen after that.
300
301 if (m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry)
302 QWindowSystemInterface::handleScreenGeometryChange(screen(), m_geometry, m_availableGeometry);
303}
304
305void QIOSScreen::setUpdatesPaused(bool paused)
306{
307 m_displayLink.paused = paused;
308}
309
310void QIOSScreen::deliverUpdateRequests() const
311{
312 bool pauseUpdates = true;
313
314 if (shouldPauseDisplayLinkWhenInactive()
315 && QGuiApplication::applicationState() != Qt::ApplicationActive) {
316 // The applicationWillResignActive documentation describes that the app
317 // should "use this method to pause ongoing tasks, disable timers, and
318 // throttle down OpenGL ES frame rates", so we skip update request
319 // delivery if the app is not active. Once it becomes active again
320 // we re-try the update request delivery (see QIOSScreen constructor).
321 qCDebug(lcQpaApplication) << "Skipping update request delivery and pausing display link";
322 m_displayLink.paused = true;
323 return;
324 }
325
326 QList<QWindow*> windows = QGuiApplication::allWindows();
327 for (int i = 0; i < windows.size(); ++i) {
328 QWindow *window = windows.at(i);
329 if (platformScreenForWindow(window) != this)
330 continue;
331
332 QPlatformWindow *platformWindow = window->handle();
333 if (!platformWindow)
334 continue;
335
336 if (!platformWindow->hasPendingUpdateRequest())
337 continue;
338
339 platformWindow->deliverUpdateRequest();
340
341 // Another update request was triggered, keep the display link running
342 if (platformWindow->hasPendingUpdateRequest())
343 pauseUpdates = false;
344 }
345
346 // Pause the display link if there are no pending update requests
347 m_displayLink.paused = pauseUpdates;
348}
349
350QRect QIOSScreen::geometry() const
351{
352 return m_geometry;
353}
354
355QRect QIOSScreen::availableGeometry() const
356{
357 return m_availableGeometry;
358}
359
360int QIOSScreen::depth() const
361{
362 return m_depth;
363}
364
365QImage::Format QIOSScreen::format() const
366{
367 return QImage::Format_ARGB32_Premultiplied;
368}
369
370QSizeF QIOSScreen::physicalSize() const
371{
372 return m_physicalSize;
373}
374
375QDpi QIOSScreen::logicalBaseDpi() const
376{
377 return QDpi(72, 72);
378}
379
380qreal QIOSScreen::devicePixelRatio() const
381{
382#if defined(Q_OS_VISIONOS)
383 // Based on what iPad app reports, and what Apple
384 // documents to be the default scale factor on
385 // visionOS, and the minimum scale for assets.
386 return 2.0;
387#else
388 return [m_uiScreen scale];
389#endif
390}
391
392qreal QIOSScreen::refreshRate() const
393{
394#if defined(Q_OS_VISIONOS)
395 return 120.0; // Based on what iPad app reports
396#else
397 return m_uiScreen.maximumFramesPerSecond;
398#endif
399}
400
401Qt::ScreenOrientation QIOSScreen::nativeOrientation() const
402{
403#if defined(Q_OS_VISIONOS)
404 // Based on iPad app reporting native bounds 1668x2388
405 return Qt::PortraitOrientation;
406#else
407 CGRect nativeBounds =
408#if defined(Q_OS_IOS)
409 m_uiScreen.nativeBounds;
410#else
411 m_uiScreen.bounds;
412#endif
413
414 // All known iOS devices have a native orientation of portrait, but to
415 // be on the safe side we compare the width and height of the bounds.
416 return nativeBounds.size.width >= nativeBounds.size.height ?
417 Qt::LandscapeOrientation : Qt::PortraitOrientation;
418#endif
419}
420
421Qt::ScreenOrientation QIOSScreen::orientation() const
422{
423 // We don't report UIDevice.currentDevice.orientation here,
424 // as that would report the actual orientation of the device,
425 // even if the orientation of the UI was locked to a subset
426 // of the possible orientations via the app's Info.plist or
427 // via [UIViewController supportedInterfaceOrientations].
428 auto *windowScene = rootViewForScreen(this).window.windowScene;
429 auto interfaceOrientation = windowScene ?
430 windowScene.interfaceOrientation : UIInterfaceOrientationUnknown;
431
432 // FIXME: On visionOS the interface orientation is reported
433 // as portrait, which seems strange, but at least it matches
434 // what we report as the native orientation.
435
436 switch (interfaceOrientation) {
437 case UIInterfaceOrientationPortrait:
438 return Qt::PortraitOrientation;
439 case UIInterfaceOrientationPortraitUpsideDown:
440 return Qt::InvertedPortraitOrientation;
441 case UIInterfaceOrientationLandscapeLeft:
442 return Qt::LandscapeOrientation;
443 case UIInterfaceOrientationLandscapeRight:
444 return Qt::InvertedLandscapeOrientation;
445 case UIInterfaceOrientationUnknown:
446 default:
447 // Fall back to the primary orientation, but with a concrete
448 // orientation instead of Qt::PrimaryOrientation, as when we
449 // report orientation changes the primary orientation has not
450 // been updated yet, so user's can't query it in response.
451 return m_geometry.width() >= m_geometry.height() ?
452 Qt::LandscapeOrientation : Qt::PortraitOrientation;
453 }
454}
455
456QPixmap QIOSScreen::grabWindow(WId window, int x, int y, int width, int height) const
457{
458 if (window && ![reinterpret_cast<id>(window) isKindOfClass:[UIView class]])
459 return QPixmap();
460
461 UIView *view = window ? reinterpret_cast<UIView *>(window)
462 : rootViewForScreen(this);
463
464 if (width < 0)
465 width = qMax(view.bounds.size.width - x, CGFloat(0));
466 if (height < 0)
467 height = qMax(view.bounds.size.height - y, CGFloat(0));
468
469 CGRect captureRect = [view.window convertRect:CGRectMake(x, y, width, height) fromView:view];
470 captureRect = CGRectIntersection(captureRect, view.window.bounds);
471
472 QMacAutoReleasePool autoReleasePool;
473
474 UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
475 format.opaque = NO;
476 format.scale = devicePixelRatio();
477 // Could be adjusted to support HDR in the future.
478 format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
479
480 UIGraphicsImageRenderer *renderer = [[[UIGraphicsImageRenderer alloc]
481 initWithSize:captureRect.size format:format]
482 autorelease];
483
484 UIImage *screenshot = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
485 CGContextRef context = rendererContext.CGContext;
486 CGContextTranslateCTM(context, -captureRect.origin.x, -captureRect.origin.y);
487
488 // Draws the complete view hierarchy of view.window into the given rect, which
489 // needs to be the same aspect ratio as the view.window's size. Since we've
490 // translated the graphics context, and are potentially drawing into a smaller
491 // context than the full window, the resulting image will be a subsection of the
492 // full screen.
493 //
494 // TODO: Should only be run on the UI thread in the future. At
495 // the time of writing, QScreen::grabWindow doesn't include any
496 // requirements as to which thread it can be called from.
497 [view.window drawViewHierarchyInRect:view.window.bounds afterScreenUpdates:NO];
498 }];
499
500 return QPixmap::fromImage(qt_mac_toQImage(screenshot.CGImage));
501}
502
503bool QIOSScreen::shouldPauseDisplayLinkWhenInactive() const
504{
505 // iPad 7 with iOS 18 has been observed to crash during swapBuffers
506 // if the user enters split-view mode and drags the divider around.
507 // This is likely a GL driver issue. To reduce the chance of hitting
508 // this crash we pause the display link when the app is inactive.
509 // We limit it to A10 devices, as the crash has not been observed on
510 // other SoCs. See QTBUG-132314 for more information.
511
512 static const bool isA10GPU = []{
513 auto device = MTLCreateSystemDefaultDevice();
514 return [device.name containsString:@"A10"];
515 }();
516
517 static const bool pauseWhenInactive = qEnvironmentVariableIntegerValue(
518 "QT_IOS_PAUSE_DISPLAY_LINK_WHEN_INACTIVE").value_or(isA10GPU);
519 return pauseWhenInactive;
520}
521
522#if !defined(Q_OS_VISIONOS)
523UIScreen *QIOSScreen::uiScreen() const
524{
525 return m_uiScreen;
526}
527#endif
528
529QT_END_NAMESPACE
530
531#include "moc_qiosscreen.cpp"
static QString deviceModelIdentifier()
Returns the model identifier of the device.