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
qnsview_drawing.mm
Go to the documentation of this file.
1// Copyright (C) 2018 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// This file is included from qnsview.mm, and only used to organize the code
6
7@implementation QContainerLayer {
8 CALayer *m_contentLayer;
9}
10- (instancetype)initWithContentLayer:(CALayer *)contentLayer
11{
12 if ((self = [super init])) {
13 m_contentLayer = contentLayer;
14 [self addSublayer:contentLayer];
15 }
16 return self;
17}
18
19- (CALayer*)contentLayer
20{
21 return m_contentLayer;
22}
23
24- (void)layoutSublayers
25{
26 // Layout the content layer explicitly, as using a autoresizingMask
27 // of kCALayerWidthSizable | kCALayerHeightSizable has been seen to
28 // drift out of sync, resulting in a content layer larger than its
29 // container layer.
30 m_contentLayer.frame = self.bounds;
31}
32
33- (void)setNeedsDisplay
34{
35 [self setNeedsDisplayInRect:CGRectInfinite];
36}
37
38- (void)setNeedsDisplayInRect:(CGRect)rect
39{
40 [super setNeedsDisplayInRect:rect];
41 [self.contentLayer setNeedsDisplayInRect:rect];
42}
43@end
44
45@implementation QNSView (Drawing)
46
47- (void)initDrawing
48{
49 // Pick up and persist requested color space from surface format
50 const QSurfaceFormat surfaceFormat = m_platformWindow->format();
51 if (QColorSpace colorSpace = surfaceFormat.colorSpace(); colorSpace.isValid()) {
52 NSData *iccData = colorSpace.iccProfile().toNSData();
53 self.colorSpace = [[[NSColorSpace alloc] initWithICCProfileData:iccData] autorelease];
54 }
55
56 // Trigger creation of the layer
57 self.wantsLayer = YES;
58}
59
60- (BOOL)isOpaque
61{
62 if (!m_platformWindow)
63 return true;
64 return m_platformWindow->isOpaque();
65}
66
67- (BOOL)isFlipped
68{
69 return YES;
70}
71
72- (NSColorSpace*)colorSpace
73{
74 // If no explicit color space was set, use the NSWindow's color space
75 return m_colorSpace ? m_colorSpace : self.window.colorSpace;
76}
77
78// ----------------------- Layer setup -----------------------
79
80- (BOOL)shouldUseMetalLayer
81{
82 // MetalSurface needs a layer, and so does VulkanSurface (via MoltenVK)
83 QSurface::SurfaceType surfaceType = m_platformWindow->window()->surfaceType();
84 return surfaceType == QWindow::MetalSurface || surfaceType == QWindow::VulkanSurface;
85}
86
87/*
88 This method is called by AppKit when layer-backing is requested by
89 setting wantsLayer too YES (via -[NSView _updateLayerBackedness]),
90 or in cases where AppKit itself decides that a view should be
91 layer-backed.
92
93 Note however that some code paths in AppKit will not go via this
94 method for creating the backing layer, and will instead create the
95 layer manually, and just call setLayer. An example of this is when
96 an NSOpenGLContext is attached to a view, in which case AppKit will
97 create a new layer in NSOpenGLContextSetLayerOnViewIfNecessary.
98
99 For this reason we leave the implementation of this override as
100 minimal as possible, only focusing on creating the appropriate
101 layer type, and then leave it up to setLayer to do the work of
102 making sure the layer is set up correctly.
103*/
104- (CALayer *)makeBackingLayer
105{
106 if ([self shouldUseMetalLayer]) {
107 // Check if Metal is supported. If it isn't then it's most likely
108 // too late at this point and the QWindow will be non-functional,
109 // but we can at least print a warning.
110 if ([MTLCreateSystemDefaultDevice() autorelease]) {
111 static bool allowPresentsWithTransaction =
112 !qEnvironmentVariableIsSet("QT_MTL_NO_TRANSACTION");
113 return allowPresentsWithTransaction ?
114 [QMetalLayer layer] : [CAMetalLayer layer];
115 } else {
116 qCWarning(lcQpaDrawing) << "Failed to create QWindow::MetalSurface."
117 << "Metal is not supported by any of the GPUs in this system.";
118 }
119 }
120
121 // We handle drawing via displayLayer instead of drawRect or updateLayer,
122 // as the latter two do not work for CAMetalLayer. And we handle content
123 // scale manually for the same reason. Which means we don't really need
124 // NSViewBackingLayer. In fact it just gets in the way, by assuming that
125 // if we don't have a drawRect function we "draw nothing".
126 return [CALayer layer];
127}
128
129/*
130 This method is called by AppKit whenever the view is asked to change
131 its layer, which can happen both as a result of enabling layer-backing,
132 or when a layer is set explicitly. The latter can happen both when a
133 view is layer-hosting, or when AppKit internals are switching out the
134 layer-backed view, as described above for makeBackingLayer.
135*/
136- (void)setLayer:(CALayer *)layer
137{
138 qCDebug(lcQpaDrawing) << "Making" << self
139 << (self.wantsLayer ? "layer-backed" : "layer-hosted")
140 << "with" << layer;
141
142 if (layer.delegate && layer.delegate != self) {
143 qCWarning(lcQpaDrawing) << "Layer already has delegate" << layer.delegate
144 << "This delegate is responsible for all view updates for" << self;
145 } else {
146 layer.delegate = self;
147 }
148
149 layer.name = @"Qt content layer";
150
151 static const bool containerLayerOptOut = qEnvironmentVariableIsSet("QT_MAC_NO_CONTAINER_LAYER");
152 if (m_platformWindow->window()->surfaceType() != QSurface::OpenGLSurface && !containerLayerOptOut) {
153 qCDebug(lcQpaDrawing) << "Wrapping content layer" << layer << "in container layer";
154 auto *containerLayer = [[[QContainerLayer alloc] initWithContentLayer:layer] autorelease];
155 containerLayer.name = @"Qt container layer";
156 containerLayer.delegate = self;
157 layer = containerLayer;
158 }
159
160 [super setLayer:layer];
161
162 [self propagateBackingProperties];
163
164 if (self.opaque && lcQpaDrawing().isDebugEnabled()) {
165 // If the view claims to be opaque we expect it to fill the entire
166 // layer with content, in which case we want to detect any areas
167 // where it doesn't.
168 layer.backgroundColor = NSColor.magentaColor.CGColor;
169 }
170}
171
172// ----------------------- Layer updates -----------------------
173
174- (NSViewLayerContentsRedrawPolicy)layerContentsRedrawPolicy
175{
176 // We need to set this explicitly since the super implementation
177 // returns LayerContentsRedrawNever for custom layers like CAMetalLayer.
178 return NSViewLayerContentsRedrawDuringViewResize;
179}
180
181- (NSViewLayerContentsPlacement)layerContentsPlacement
182{
183 // Always place the layer at top left without any automatic scaling.
184 // This will highlight situations where we're missing content for the
185 // layer by not responding to the displayLayer: request synchronously.
186 // It also allows us to re-use larger layers when resizing a window down.
187 return NSViewLayerContentsPlacementTopLeft;
188}
189
190- (void)viewDidChangeBackingProperties
191{
192 qCDebug(lcQpaDrawing) << "Backing properties changed for" << self;
193
194 if (!m_platformWindow)
195 return;
196
197 [self propagateBackingProperties];
198
199 // Ideally we would plumb this situation through QPA in a way that lets
200 // clients invalidate their own caches, recreate QBackingStore, etc.
201
202 // QPA supports DPR (scale) change notifications. We are not sure
203 // based on this event that it is the scale that has changed (it
204 // could be the color space), however QPA will determine if it has
205 // actually changed.
206 QWindowSystemInterface::handleWindowDevicePixelRatioChanged
207 <QWindowSystemInterface::SynchronousDelivery>(m_platformWindow->window());
208
209 // Trigger an expose, and let QCocoaBackingStore deal with
210 // buffer invalidation internally.
211 [self setNeedsDisplay:YES];
212}
213
214- (void)propagateBackingProperties
215{
216 if (!self.layer)
217 return;
218
219 // We expect clients to fill the layer with retina aware content,
220 // based on the devicePixelRatio of the QWindow, so we set the
221 // layer's content scale to match that. By going via devicePixelRatio
222 // instead of applying the NSWindow's backingScaleFactor, we also take
223 // into account OpenGL views with wantsBestResolutionOpenGLSurface set
224 // to NO. In this case the window will have a backingScaleFactor of 2,
225 // but the QWindow will have a devicePixelRatio of 1.
226 auto devicePixelRatio = m_platformWindow->devicePixelRatio();
227 auto *contentLayer = m_platformWindow->contentLayer();
228 qCDebug(lcQpaDrawing) << "Updating" << contentLayer << "content scale to" << devicePixelRatio;
229 contentLayer.contentsScale = devicePixelRatio;
230
231 if ([contentLayer isKindOfClass:CAMetalLayer.class]) {
232 CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(contentLayer);
233 metalLayer.colorspace = self.colorSpace.CGColorSpace;
234 qCDebug(lcQpaDrawing) << "Set" << metalLayer << "color space to" << metalLayer.colorspace;
235 }
236}
237
238/*
239 This method is called by AppKit to determine whether it should update
240 the contentScale of the layer to match the window backing scale.
241
242 We always return NO since we're updating the contents scale manually.
243*/
244- (BOOL)layer:(CALayer *)layer shouldInheritContentsScale:(CGFloat)scale fromWindow:(NSWindow *)window
245{
246 Q_UNUSED(layer);
247 Q_UNUSED(scale);
248 Q_UNUSED(window);
249 return NO;
250}
251
252// ----------------------- Draw callbacks -----------------------
253
254/*
255 We set our view up as the layer's delegate, which means we get
256 first dibs on displaying the layer, without needing to go through
257 updateLayer or drawRect.
258*/
259- (void)displayLayer:(CALayer *)layer
260{
261 if (auto *containerLayer = qt_objc_cast<QContainerLayer*>(layer)) {
262 qCDebug(lcQpaDrawing) << "Skipping display of" << containerLayer
263 << "as display is handled by content layer" << containerLayer.contentLayer;
264 return;
265 }
266
267 if (!m_platformWindow)
268 return;
269
270 auto *qtMetalLayer = qt_objc_cast<QMetalLayer *>(layer);
271
272 if (qtMetalLayer) {
273 // Once we're done with display, even if we exit early below, we need to
274 // unlock the display lock. But we must wait to unlock the display lock
275 // until the display cycle finishes, as otherwise the render thread may
276 // step in and present before the transaction commits. The display lock
277 // is recursive, so setNeedsDisplay can be safely called in the meantime
278 // without any issue.
279 QMetaObject::invokeMethod(m_platformWindow, [qtMetalLayer]{
280 qCDebug(lcMetalLayer) << "Unlocking" << qtMetalLayer << "after finishing display-cycle";
281 qtMetalLayer.displayLock.unlock();
282 }, Qt::QueuedConnection);
283 }
284
285 if (!NSThread.isMainThread) {
286 // Qt is calling AppKit APIs such as -[NSOpenGLContext setView:] on secondary threads,
287 // which we shouldn't do. This may result in AppKit (wrongly) triggering a display on
288 // the thread where we made the call, so block it here and defer to the main thread.
289 qCWarning(lcQpaDrawing) << "Display non non-main thread! Deferring to main thread";
290 dispatch_async(dispatch_get_main_queue(), ^{ self.needsDisplay = YES; });
291 return;
292 }
293
294 if (m_platformWindow->m_deliveringUpdateRequest) {
295 // In rare cases delivering an update request might trigger a synchronous display,
296 // for example when calling -[NSOpenGLContext update] as part of rendering a frame,
297 // if using the software GL backend on macOS 26. Our rendering stack does not deal
298 // well with this reentrancy, and it also causes crashes the macOS GL driver.
299 qCWarning(lcQpaDrawing) << "Asked to display during update-request delivery. Deferring.";
300 dispatch_async(dispatch_get_main_queue(), ^{ self.needsDisplay = YES; });
301 return;
302 }
303
304 auto *currentScreen = static_cast<QCocoaScreen*>(m_platformWindow->screen());
305 if (!currentScreen || !currentScreen->isOnline()) {
306 qCWarning(lcQpaDrawing) << "Display requested for non-online display" << currentScreen
307 << "Deferring to next runloop pass";
308 dispatch_async(dispatch_get_main_queue(), ^{ self.needsDisplay = YES; });
309 return;
310 }
311
312 const auto handleExposeEvent = [&]{
313 const auto bounds = QRectF::fromCGRect(self.bounds).toRect();
314 qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window() << bounds;
315 m_platformWindow->handleExposeEvent(bounds);
316 };
317
318 if (qtMetalLayer) {
319 const bool presentedWithTransaction = qtMetalLayer.presentsWithTransaction;
320 qtMetalLayer.presentsWithTransaction = YES;
321
322 handleExposeEvent();
323
324 {
325 // Clearing the mainThreadPresentation below will auto-release the
326 // block held by the property, which in turn holds on to drawables,
327 // so we want to clean up as soon as possible, to prevent stalling
328 // when requesting new drawables. But merely referencing the block
329 // below for the nil-check will make another auto-released copy of
330 // the block, so the scope of the auto-release pool needs to include
331 // that check as well.
332 QMacAutoReleasePool pool;
333
334 // If the expose event resulted in a secondary thread requesting that its
335 // drawable should be presented on the main thread with transaction, do so.
336 if (auto mainThreadPresentation = qtMetalLayer.mainThreadPresentation) {
337 mainThreadPresentation();
338 qtMetalLayer.mainThreadPresentation = nil;
339 }
340 }
341
342 qtMetalLayer.presentsWithTransaction = presentedWithTransaction;
343 } else {
344 handleExposeEvent();
345 }
346}
347
348@end