1/**************************************************************************** 2** 3** Copyright (C) 2018 The Qt Company Ltd. 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the plugins of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39 40// This file is included from qnsview.mm, and only used to organize the code 41 42@implementation QNSView (Drawing) 43 44- (void)initDrawing 45{ 46 [self updateLayerBacking]; 47} 48 49- (BOOL)isOpaque 50{ 51 if (!m_platformWindow) 52 return true; 53 return m_platformWindow->isOpaque(); 54} 55 56- (BOOL)isFlipped 57{ 58 return YES; 59} 60 61// ----------------------- Layer setup ----------------------- 62 63- (void)updateLayerBacking 64{ 65 self.wantsLayer = [self layerEnabledByMacOS] 66 || [self layerExplicitlyRequested] 67 || [self shouldUseMetalLayer]; 68} 69 70- (BOOL)layerEnabledByMacOS 71{ 72 // AppKit has its own logic for this, but if we rely on that, our layers are created 73 // by AppKit at a point where we've already set up other parts of the platform plugin 74 // based on the presence of layers or not. Once we've rewritten these parts to support 75 // dynamically picking up layer enablement we can let AppKit do its thing. 76 77 if (QMacVersion::currentRuntime() >= QOperatingSystemVersion::MacOSBigSur) 78 return true; // Big Sur always enables layer-backing, regardless of SDK 79 80 if (QMacVersion::currentRuntime() >= QOperatingSystemVersion::MacOSMojave 81 && QMacVersion::buildSDK() >= QOperatingSystemVersion::MacOSMojave) 82 return true; // Mojave and Catalina enable layers based on the app's SDK 83 84 return false; // Prior versions needed explicitly enabled layer backing 85} 86 87- (BOOL)layerExplicitlyRequested 88{ 89 static bool wantsLayer = [&]() { 90 int wantsLayer = qt_mac_resolveOption(-1, m_platformWindow->window(), 91 "_q_mac_wantsLayer", "QT_MAC_WANTS_LAYER"); 92 93 if (wantsLayer != -1 && [self layerEnabledByMacOS]) { 94 qCWarning(lcQpaDrawing) << "Layer-backing cannot be explicitly controlled on 10.14 when built against the 10.14 SDK"; 95 return true; 96 } 97 98 return wantsLayer == 1; 99 }(); 100 101 return wantsLayer; 102} 103 104- (BOOL)shouldUseMetalLayer 105{ 106 // MetalSurface needs a layer, and so does VulkanSurface (via MoltenVK) 107 QSurface::SurfaceType surfaceType = m_platformWindow->window()->surfaceType(); 108 return surfaceType == QWindow::MetalSurface || surfaceType == QWindow::VulkanSurface; 109} 110 111/* 112 This method is called by AppKit when layer-backing is requested by 113 setting wantsLayer too YES (via -[NSView _updateLayerBackedness]), 114 or in cases where AppKit itself decides that a view should be 115 layer-backed. 116 117 Note however that some code paths in AppKit will not go via this 118 method for creating the backing layer, and will instead create the 119 layer manually, and just call setLayer. An example of this is when 120 an NSOpenGLContext is attached to a view, in which case AppKit will 121 create a new layer in NSOpenGLContextSetLayerOnViewIfNecessary. 122 123 For this reason we leave the implementation of this override as 124 minimal as possible, only focusing on creating the appropriate 125 layer type, and then leave it up to setLayer to do the work of 126 making sure the layer is set up correctly. 127*/ 128- (CALayer *)makeBackingLayer 129{ 130 if ([self shouldUseMetalLayer]) { 131 // Check if Metal is supported. If it isn't then it's most likely 132 // too late at this point and the QWindow will be non-functional, 133 // but we can at least print a warning. 134 if ([MTLCreateSystemDefaultDevice() autorelease]) { 135 return [CAMetalLayer layer]; 136 } else { 137 qCWarning(lcQpaDrawing) << "Failed to create QWindow::MetalSurface." 138 << "Metal is not supported by any of the GPUs in this system."; 139 } 140 } 141 142 return [super makeBackingLayer]; 143} 144 145/* 146 This method is called by AppKit whenever the view is asked to change 147 its layer, which can happen both as a result of enabling layer-backing, 148 or when a layer is set explicitly. The latter can happen both when a 149 view is layer-hosting, or when AppKit internals are switching out the 150 layer-backed view, as described above for makeBackingLayer. 151*/ 152- (void)setLayer:(CALayer *)layer 153{ 154 qCDebug(lcQpaDrawing) << "Making" << self 155 << (self.wantsLayer ? "layer-backed" : "layer-hosted") 156 << "with" << layer << "due to being" << ([self layerExplicitlyRequested] ? "explicitly requested" 157 : [self shouldUseMetalLayer] ? "needed by surface type" : "enabled by macOS"); 158 159 if (layer.delegate && layer.delegate != self) { 160 qCWarning(lcQpaDrawing) << "Layer already has delegate" << layer.delegate 161 << "This delegate is responsible for all view updates for" << self; 162 } else { 163 layer.delegate = self; 164 } 165 166 [super setLayer:layer]; 167 168 // When adding a view to a view hierarchy the backing properties will change 169 // which results in updating the contents scale, but in case of switching the 170 // layer on a view that's already in a view hierarchy we need to manually ensure 171 // the scale is up to date. 172 if (self.superview) 173 [self updateLayerContentsScale]; 174 175 if (self.opaque && lcQpaDrawing().isDebugEnabled()) { 176 // If the view claims to be opaque we expect it to fill the entire 177 // layer with content, in which case we want to detect any areas 178 // where it doesn't. 179 layer.backgroundColor = NSColor.magentaColor.CGColor; 180 } 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 (self.layer) 207 [self updateLayerContentsScale]; 208 209 // Ideally we would plumb this situation through QPA in a way that lets 210 // clients invalidate their own caches, recreate QBackingStore, etc. 211 // For now we trigger an expose, and let QCocoaBackingStore deal with 212 // buffer invalidation internally. 213 [self setNeedsDisplay:YES]; 214} 215 216- (void)updateLayerContentsScale 217{ 218 // We expect clients to fill the layer with retina aware content, 219 // based on the devicePixelRatio of the QWindow, so we set the 220 // layer's content scale to match that. By going via devicePixelRatio 221 // instead of applying the NSWindow's backingScaleFactor, we also take 222 // into account OpenGL views with wantsBestResolutionOpenGLSurface set 223 // to NO. In this case the window will have a backingScaleFactor of 2, 224 // but the QWindow will have a devicePixelRatio of 1. 225 auto devicePixelRatio = m_platformWindow->devicePixelRatio(); 226 qCDebug(lcQpaDrawing) << "Updating" << self.layer << "content scale to" << devicePixelRatio; 227 self.layer.contentsScale = devicePixelRatio; 228} 229 230/* 231 This method is called by AppKit to determine whether it should update 232 the contentScale of the layer to match the window backing scale. 233 234 We always return NO since we're updating the contents scale manually. 235*/ 236- (BOOL)layer:(CALayer *)layer shouldInheritContentsScale:(CGFloat)scale fromWindow:(NSWindow *)window 237{ 238 Q_UNUSED(layer); Q_UNUSED(scale); Q_UNUSED(window); 239 return NO; 240} 241 242// ----------------------- Draw callbacks ----------------------- 243 244/* 245 This method is called by AppKit for the non-layer case, where we are 246 drawing into the NSWindow's surface. 247*/ 248- (void)drawRect:(NSRect)dirtyBoundingRect 249{ 250 Q_ASSERT_X(!self.layer, "QNSView", 251 "The drawRect code path should not be hit when we are layer backed"); 252 253 if (!m_platformWindow) 254 return; 255 256 QRegion exposedRegion; 257 const NSRect *dirtyRects; 258 NSInteger numDirtyRects; 259 [self getRectsBeingDrawn:&dirtyRects count:&numDirtyRects]; 260 for (int i = 0; i < numDirtyRects; ++i) 261 exposedRegion += QRectF::fromCGRect(dirtyRects[i]).toRect(); 262 263 if (exposedRegion.isEmpty()) 264 exposedRegion = QRectF::fromCGRect(dirtyBoundingRect).toRect(); 265 266 qCDebug(lcQpaDrawing) << "[QNSView drawRect:]" << m_platformWindow->window() << exposedRegion; 267 m_platformWindow->handleExposeEvent(exposedRegion); 268} 269 270/* 271 This method is called by AppKit when we are layer-backed, where 272 we are drawing into the layer. 273*/ 274- (void)displayLayer:(CALayer *)layer 275{ 276 Q_ASSERT_X(self.layer && layer == self.layer, "QNSView", 277 "The displayLayer code path should only be hit for our own layer"); 278 279 if (!m_platformWindow) 280 return; 281 282 if (!NSThread.isMainThread) { 283 // Qt is calling AppKit APIs such as -[NSOpenGLContext setView:] on secondary threads, 284 // which we shouldn't do. This may result in AppKit (wrongly) triggering a display on 285 // the thread where we made the call, so block it here and defer to the main thread. 286 qCWarning(lcQpaDrawing) << "Display non non-main thread! Deferring to main thread"; 287 dispatch_async(dispatch_get_main_queue(), ^{ self.needsDisplay = YES; }); 288 return; 289 } 290 291 qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window(); 292 m_platformWindow->handleExposeEvent(QRectF::fromCGRect(self.bounds).toRect()); 293} 294 295@end 296