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