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