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