1/**************************************************************************** 2** 3** Copyright (C) 2016 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#include "qcocoabackingstore.h" 41 42#include "qcocoawindow.h" 43#include "qcocoahelpers.h" 44 45#include <QtCore/qmath.h> 46#include <QtGui/qpainter.h> 47 48#include <QuartzCore/CATransaction.h> 49 50QT_BEGIN_NAMESPACE 51 52QCocoaBackingStore::QCocoaBackingStore(QWindow *window) 53 : QRasterBackingStore(window) 54{ 55} 56 57QCFType<CGColorSpaceRef> QCocoaBackingStore::colorSpace() const 58{ 59 NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view(); 60 return QCFType<CGColorSpaceRef>::constructFromGet(view.window.colorSpace.CGColorSpace); 61} 62 63// ---------------------------------------------------------------------------- 64 65QNSWindowBackingStore::QNSWindowBackingStore(QWindow *window) 66 : QCocoaBackingStore(window) 67{ 68 // Choose an appropriate window depth based on the requested surface format. 69 // On deep color displays the default bit depth is 16-bit, so unless we need 70 // that level of precision we opt out of it (and the expensive RGB32 -> RGB64 71 // conversions that come with it if our backingstore depth does not match). 72 73 NSWindow *nsWindow = static_cast<QCocoaWindow *>(window->handle())->view().window; 74 auto colorSpaceName = NSColorSpaceFromDepth(nsWindow.depthLimit); 75 76 static const int kDefaultBitDepth = 8; 77 auto surfaceFormat = window->requestedFormat(); 78 auto bitsPerSample = qMax(kDefaultBitDepth, qMax(surfaceFormat.redBufferSize(), 79 qMax(surfaceFormat.greenBufferSize(), surfaceFormat.blueBufferSize()))); 80 81 // NSBestDepth does not seem to guarantee a window depth deep enough for the 82 // given bits per sample, even if documented as such. For example, requesting 83 // 10 bits per sample will not give us a 16-bit format, even if that's what's 84 // available. Work around this by manually bumping the bit depth. 85 bitsPerSample = !(bitsPerSample & (bitsPerSample - 1)) 86 ? bitsPerSample : qNextPowerOfTwo(bitsPerSample); 87 88 auto bestDepth = NSBestDepth(colorSpaceName, bitsPerSample, 0, NO, nullptr); 89 90 // Disable dynamic depth limit, otherwise our depth limit will be overwritten 91 // by AppKit if the window moves to a screen with a different depth. We call 92 // this before setting the depth limit, as the call will reset the depth to 0. 93 [nsWindow setDynamicDepthLimit:NO]; 94 95 qCDebug(lcQpaBackingStore) << "Using" << NSBitsPerSampleFromDepth(bestDepth) 96 << "bit window depth for" << nsWindow; 97 98 nsWindow.depthLimit = bestDepth; 99} 100 101QNSWindowBackingStore::~QNSWindowBackingStore() 102{ 103} 104 105bool QNSWindowBackingStore::windowHasUnifiedToolbar() const 106{ 107 Q_ASSERT(window()->handle()); 108 return static_cast<QCocoaWindow *>(window()->handle())->m_drawContentBorderGradient; 109} 110 111QImage::Format QNSWindowBackingStore::format() const 112{ 113 if (windowHasUnifiedToolbar()) 114 return QImage::Format_ARGB32_Premultiplied; 115 116 return QRasterBackingStore::format(); 117} 118 119void QNSWindowBackingStore::resize(const QSize &size, const QRegion &staticContents) 120{ 121 qCDebug(lcQpaBackingStore) << "Resize requested to" << size; 122 QRasterBackingStore::resize(size, staticContents); 123 124 // The window shadow rendered by AppKit is based on the shape/content of the 125 // NSWindow surface. Technically any flush of the backingstore can result in 126 // a potentially new shape of the window, and would need a shadow invalidation, 127 // but this is likely too expensive to do at every flush for the few cases where 128 // clients change the shape dynamically. One case where we do know that the shadow 129 // likely needs invalidation, if the window has partially transparent content, 130 // is after a resize, where AppKit's default shadow may be based on the previous 131 // window content. 132 QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window()->handle()); 133 if (cocoaWindow->isContentView() && !cocoaWindow->isOpaque()) 134 cocoaWindow->m_needsInvalidateShadow = true; 135} 136 137/*! 138 Flushes the given \a region from the specified \a window onto the 139 screen. 140 141 The \a window is the top level window represented by this backingstore, 142 or a non-transient child of that window. 143 144 If the \a window is a child window, the \a region will be in child window 145 coordinates, and the \a offset will be the child window's offset in relation 146 to the backingstore's top level window. 147*/ 148void QNSWindowBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) 149{ 150 if (m_image.isNull()) 151 return; 152 153 // Use local pool so that any stale image references are cleaned up after flushing 154 QMacAutoReleasePool pool; 155 156 const QWindow *topLevelWindow = this->window(); 157 158 Q_ASSERT(topLevelWindow->handle() && window->handle()); 159 Q_ASSERT(!topLevelWindow->handle()->isForeignWindow() && !window->handle()->isForeignWindow()); 160 161 QNSView *topLevelView = qnsview_cast(static_cast<QCocoaWindow *>(topLevelWindow->handle())->view()); 162 QNSView *view = qnsview_cast(static_cast<QCocoaWindow *>(window->handle())->view()); 163 164 if (lcQpaBackingStore().isDebugEnabled()) { 165 QString targetViewDescription; 166 if (view != topLevelView) { 167 QDebug targetDebug(&targetViewDescription); 168 targetDebug << "onto" << topLevelView << "at" << offset; 169 } 170 qCDebug(lcQpaBackingStore) << "Flushing" << region << "of" << view << qPrintable(targetViewDescription); 171 } 172 173 // Normally a NSView is drawn via drawRect, as part of the display cycle in the 174 // main runloop, via setNeedsDisplay and friends. AppKit will lock focus on each 175 // individual view, starting with the top level and then traversing any subviews, 176 // calling drawRect for each of them. This pull model results in expose events 177 // sent to Qt, which result in drawing to the backingstore and flushing it. 178 // Qt may also decide to paint and flush the backingstore via e.g. timers, 179 // or other events such as mouse events, in which case we're in a push model. 180 // If there is no focused view, it means we're in the latter case, and need 181 // to manually flush the NSWindow after drawing to its graphic context. 182 const bool drawingOutsideOfDisplayCycle = ![NSView focusView]; 183 184 // We also need to ensure the flushed view has focus, so that the graphics 185 // context is set up correctly (coordinate system, clipping, etc). Outside 186 // of the normal display cycle there is no focused view, as explained above, 187 // so we have to handle it manually. There's also a corner case inside the 188 // normal display cycle due to way QWidgetRepaintManager composits native child 189 // widgets, where we'll get a flush of a native child during the drawRect of 190 // its parent/ancestor, and the parent/ancestor being the one locked by AppKit. 191 // In this case we also need to lock and unlock focus manually. 192 const bool shouldHandleViewLockManually = [NSView focusView] != view; 193 if (shouldHandleViewLockManually && ![view lockFocusIfCanDraw]) { 194 qWarning() << "failed to lock focus of" << view; 195 return; 196 } 197 198 const qreal devicePixelRatio = m_image.devicePixelRatio(); 199 200 // If the flushed window is a content view, and we're filling the drawn area 201 // completely, or it doesn't have a window background we need to preserve, 202 // we can get away with copying instead of blending the backing store. 203 QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()); 204 const NSCompositingOperation compositingOperation = cocoaWindow->isContentView() 205 && (cocoaWindow->isOpaque() || view.window.backgroundColor == NSColor.clearColor) 206 ? NSCompositingOperationCopy : NSCompositingOperationSourceOver; 207 208#ifdef QT_DEBUG 209 static bool debugBackingStoreFlush = [[NSUserDefaults standardUserDefaults] 210 boolForKey:@"QtCocoaDebugBackingStoreFlush"]; 211#endif 212 213 // ------------------------------------------------------------------------- 214 215 // The current contexts is typically a NSWindowGraphicsContext, but can be 216 // NSBitmapGraphicsContext e.g. when debugging the view hierarchy in Xcode. 217 // If we need to distinguish things here in the future, we can use e.g. 218 // [NSGraphicsContext drawingToScreen], or the attributes of the context. 219 NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; 220 Q_ASSERT_X(graphicsContext, "QCocoaBackingStore", 221 "Focusing the view should give us a current graphics context"); 222 223 // Tag backingstore image with color space based on the window. 224 // Note: This does not copy the underlying image data. 225 QCFType<CGImageRef> cgImage = CGImageCreateCopyWithColorSpace( 226 QCFType<CGImageRef>(m_image.toCGImage()), colorSpace()); 227 228 // Create temporary image to use for blitting, without copying image data 229 NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease]; 230 231 QRegion clippedRegion = region; 232 for (QWindow *w = window; w; w = w->parent()) { 233 if (!w->mask().isEmpty()) { 234 clippedRegion &= w == window ? w->mask() 235 : w->mask().translated(window->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)))); 236 } 237 } 238 239 for (const QRect &viewLocalRect : clippedRegion) { 240 QPoint backingStoreOffset = viewLocalRect.topLeft() + offset; 241 QRect backingStoreRect(backingStoreOffset * devicePixelRatio, viewLocalRect.size() * devicePixelRatio); 242 if (graphicsContext.flipped) // Flip backingStoreRect to match graphics context 243 backingStoreRect.moveTop(m_image.height() - (backingStoreRect.y() + backingStoreRect.height())); 244 245 CGRect viewRect = viewLocalRect.toCGRect(); 246 247 [backingStoreImage drawInRect:viewRect fromRect:backingStoreRect.toCGRect() 248 operation:compositingOperation fraction:1.0 respectFlipped:YES hints:nil]; 249 250#ifdef QT_DEBUG 251 if (Q_UNLIKELY(debugBackingStoreFlush)) { 252 [[NSColor colorWithCalibratedRed:drand48() green:drand48() blue:drand48() alpha:0.3] set]; 253 [NSBezierPath fillRect:viewRect]; 254 255 if (drawingOutsideOfDisplayCycle) { 256 [[[NSColor magentaColor] colorWithAlphaComponent:0.5] set]; 257 [NSBezierPath strokeLineFromPoint:viewLocalRect.topLeft().toCGPoint() 258 toPoint:viewLocalRect.bottomRight().toCGPoint()]; 259 } 260 } 261#endif 262 } 263 264 // ------------------------------------------------------------------------- 265 266 if (shouldHandleViewLockManually) 267 [view unlockFocus]; 268 269 if (drawingOutsideOfDisplayCycle) { 270 redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]); 271 [view.window flushWindow]; 272 } 273 274 275 // Done flushing to NSWindow backingstore 276 277 QCocoaWindow *topLevelCocoaWindow = static_cast<QCocoaWindow *>(topLevelWindow->handle()); 278 if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) { 279 qCDebug(lcQpaBackingStore) << "Invalidating window shadow for" << topLevelCocoaWindow; 280 [topLevelView.window invalidateShadow]; 281 topLevelCocoaWindow->m_needsInvalidateShadow = false; 282 } 283} 284 285/* 286 When drawing outside of the display cycle, which Qt Widget does a lot, 287 we end up drawing over the NSThemeFrame, losing the rounded corners of 288 windows in the process. 289 290 To work around this, until we've enabled updates via setNeedsDisplay and/or 291 enabled layer-backed views, we ask the NSWindow to redraw the bottom corners 292 if they intersect with the flushed region. 293 294 This is the same logic used internally by e.g [NSView displayIfNeeded], 295 [NSRulerView _scrollToMatchContentView], and [NSClipView _immediateScrollToPoint:], 296 as well as the workaround used by WebKit to fix a similar bug: 297 298 https://trac.webkit.org/changeset/85376/webkit 299*/ 300void QNSWindowBackingStore::redrawRoundedBottomCorners(CGRect windowRect) const 301{ 302#if !defined(QT_APPLE_NO_PRIVATE_APIS) 303 Q_ASSERT(this->window()->handle()); 304 NSWindow *window = static_cast<QCocoaWindow *>(this->window()->handle())->nativeWindow(); 305 306 static SEL intersectBottomCornersWithRect = NSSelectorFromString( 307 [NSString stringWithFormat:@"_%s%s:", "intersectBottomCorners", "WithRect"]); 308 if (NSMethodSignature *signature = [window methodSignatureForSelector:intersectBottomCornersWithRect]) { 309 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; 310 invocation.target = window; 311 invocation.selector = intersectBottomCornersWithRect; 312 [invocation setArgument:&windowRect atIndex:2]; 313 [invocation invoke]; 314 315 NSRect cornerOverlap = NSZeroRect; 316 [invocation getReturnValue:&cornerOverlap]; 317 if (!NSIsEmptyRect(cornerOverlap)) { 318 static SEL maskRoundedBottomCorners = NSSelectorFromString( 319 [NSString stringWithFormat:@"_%s%s:", "maskRounded", "BottomCorners"]); 320 if ((signature = [window methodSignatureForSelector:maskRoundedBottomCorners])) { 321 invocation = [NSInvocation invocationWithMethodSignature:signature]; 322 invocation.target = window; 323 invocation.selector = maskRoundedBottomCorners; 324 [invocation setArgument:&cornerOverlap atIndex:2]; 325 [invocation invoke]; 326 } 327 } 328 } 329#else 330 Q_UNUSED(windowRect); 331#endif 332} 333 334// ---------------------------------------------------------------------------- 335 336QCALayerBackingStore::QCALayerBackingStore(QWindow *window) 337 : QCocoaBackingStore(window) 338{ 339 qCDebug(lcQpaBackingStore) << "Creating QCALayerBackingStore for" << window; 340 m_buffers.resize(1); 341 342 observeBackingPropertiesChanges(); 343 window->installEventFilter(this); 344} 345 346QCALayerBackingStore::~QCALayerBackingStore() 347{ 348} 349 350void QCALayerBackingStore::observeBackingPropertiesChanges() 351{ 352 Q_ASSERT(window()->handle()); 353 NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view(); 354 m_backingPropertiesObserver = QMacNotificationObserver(view.window, 355 NSWindowDidChangeBackingPropertiesNotification, [this]() { 356 backingPropertiesChanged(); 357 }); 358} 359 360bool QCALayerBackingStore::eventFilter(QObject *watched, QEvent *event) 361{ 362 Q_ASSERT(watched == window()); 363 364 if (event->type() == QEvent::PlatformSurface) { 365 auto *surfaceEvent = static_cast<QPlatformSurfaceEvent*>(event); 366 if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) 367 observeBackingPropertiesChanges(); 368 else 369 m_backingPropertiesObserver = QMacNotificationObserver(); 370 } 371 372 return false; 373} 374 375void QCALayerBackingStore::resize(const QSize &size, const QRegion &staticContents) 376{ 377 qCDebug(lcQpaBackingStore) << "Resize requested to" << size; 378 379 if (!staticContents.isNull()) 380 qCWarning(lcQpaBackingStore) << "QCALayerBackingStore does not support static contents"; 381 382 m_requestedSize = size; 383} 384 385void QCALayerBackingStore::beginPaint(const QRegion ®ion) 386{ 387 Q_UNUSED(region); 388 389 QMacAutoReleasePool pool; 390 391 qCInfo(lcQpaBackingStore) << "Beginning paint of" << region << "into backingstore of" << m_requestedSize; 392 393 ensureBackBuffer(); // Find an unused back buffer, or reserve space for a new one 394 395 const bool bufferWasRecreated = recreateBackBufferIfNeeded(); 396 397 m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); 398 399 // Although undocumented, QBackingStore::beginPaint expects the painted region 400 // to be cleared before use if the window has a surface format with an alpha. 401 // Fresh IOSurfaces are already cleared, so we don't need to clear those. 402 if (m_clearSurfaceOnPaint && !bufferWasRecreated && window()->format().hasAlpha()) { 403 qCDebug(lcQpaBackingStore) << "Clearing" << region << "before use"; 404 QPainter painter(m_buffers.back()->asImage()); 405 painter.setCompositionMode(QPainter::CompositionMode_Source); 406 for (const QRect &rect : region) 407 painter.fillRect(rect, Qt::transparent); 408 } 409 410 m_paintedRegion += region; 411} 412 413void QCALayerBackingStore::ensureBackBuffer() 414{ 415 if (window()->format().swapBehavior() == QSurfaceFormat::SingleBuffer) 416 return; 417 418 // The current back buffer may have been assigned to a layer in a previous flush, 419 // but we deferred the swap. Do it now if the surface has been picked up by CA. 420 if (m_buffers.back() && m_buffers.back()->isInUse() && m_buffers.back() != m_buffers.front()) { 421 qCInfo(lcQpaBackingStore) << "Back buffer has been picked up by CA, swapping to front"; 422 std::swap(m_buffers.back(), m_buffers.front()); 423 } 424 425 if (Q_UNLIKELY(lcQpaBackingStore().isDebugEnabled())) { 426 // ┌───────┬───────┬───────┬─────┬──────┐ 427 // │ front ┊ spare ┊ spare ┊ ... ┊ back │ 428 // └───────┴───────┴───────┴─────┴──────┘ 429 for (const auto &buffer : m_buffers) { 430 qCDebug(lcQpaBackingStore).nospace() << " " 431 << (buffer == m_buffers.front() ? "front" : 432 buffer == m_buffers.back() ? " back" : 433 "spare" 434 ) << ": " << buffer.get(); 435 } 436 } 437 438 // Ensure our back buffer is ready to draw into. If not, find a buffer that 439 // is not in use, or reserve space for a new buffer if none can be found. 440 for (auto &buffer : backwards(m_buffers)) { 441 if (!buffer || !buffer->isInUse()) { 442 // Buffer is okey to use, swap if necessary 443 if (buffer != m_buffers.back()) 444 std::swap(buffer, m_buffers.back()); 445 qCDebug(lcQpaBackingStore) << "Using back buffer" << m_buffers.back().get(); 446 447 static const int kMaxSwapChainDepth = 3; 448 if (m_buffers.size() > kMaxSwapChainDepth) { 449 qCDebug(lcQpaBackingStore) << "Reducing swap chain depth to" << kMaxSwapChainDepth; 450 m_buffers.erase(std::next(m_buffers.begin(), 1), std::prev(m_buffers.end(), 2)); 451 } 452 453 break; 454 } else if (buffer == m_buffers.front()) { 455 // We've exhausted the available buffers, make room for a new one 456 const int swapChainDepth = m_buffers.size() + 1; 457 qCDebug(lcQpaBackingStore) << "Available buffers exhausted, increasing swap chain depth to" << swapChainDepth; 458 m_buffers.resize(swapChainDepth); 459 break; 460 } 461 } 462 463 Q_ASSERT(!m_buffers.back() || !m_buffers.back()->isInUse()); 464} 465 466// Disabled until performance issue on 5K iMac Pro has been investigated further, 467// as rounding up during resize will typically result in full screen buffer sizes 468// and low frame rate also for smaller window sizes. 469#define USE_LAZY_BUFFER_ALLOCATION_DURING_LIVE_WINDOW_RESIZE 0 470 471bool QCALayerBackingStore::recreateBackBufferIfNeeded() 472{ 473 const QCocoaWindow *platformWindow = static_cast<QCocoaWindow *>(window()->handle()); 474 const qreal devicePixelRatio = platformWindow->devicePixelRatio(); 475 QSize requestedBufferSize = m_requestedSize * devicePixelRatio; 476 477 const NSView *backingStoreView = platformWindow->view(); 478 Q_UNUSED(backingStoreView); 479 480 auto bufferSizeMismatch = [&](const QSize requested, const QSize actual) { 481#if USE_LAZY_BUFFER_ALLOCATION_DURING_LIVE_WINDOW_RESIZE 482 if (backingStoreView.inLiveResize) { 483 // Prevent over-eager buffer allocation during window resize by reusing larger buffers 484 return requested.width() > actual.width() || requested.height() > actual.height(); 485 } 486#endif 487 return requested != actual; 488 }; 489 490 if (!m_buffers.back() || bufferSizeMismatch(requestedBufferSize, m_buffers.back()->size())) { 491#if USE_LAZY_BUFFER_ALLOCATION_DURING_LIVE_WINDOW_RESIZE 492 if (backingStoreView.inLiveResize) { 493 // Prevent over-eager buffer allocation during window resize by rounding up 494 QSize nativeScreenSize = window()->screen()->geometry().size() * devicePixelRatio; 495 requestedBufferSize = QSize(qNextPowerOfTwo(requestedBufferSize.width()), 496 qNextPowerOfTwo(requestedBufferSize.height())).boundedTo(nativeScreenSize); 497 } 498#endif 499 500 qCInfo(lcQpaBackingStore) << "Creating surface of" << requestedBufferSize 501 << "based on requested" << m_requestedSize << "and dpr =" << devicePixelRatio; 502 503 static auto pixelFormat = QImage::toPixelFormat(QImage::Format_ARGB32_Premultiplied); 504 m_buffers.back().reset(new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace())); 505 return true; 506 } 507 508 return false; 509} 510 511QPaintDevice *QCALayerBackingStore::paintDevice() 512{ 513 Q_ASSERT(m_buffers.back()); 514 return m_buffers.back()->asImage(); 515} 516 517void QCALayerBackingStore::endPaint() 518{ 519 qCInfo(lcQpaBackingStore) << "Paint ended with painted region" << m_paintedRegion; 520 m_buffers.back()->unlock(); 521} 522 523void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion ®ion, const QPoint &offset) 524{ 525 Q_UNUSED(region); 526 Q_UNUSED(offset); 527 528 if (!prepareForFlush()) 529 return; 530 531 if (flushedWindow != window()) { 532 flushSubWindow(flushedWindow); 533 return; 534 } 535 536 QMacAutoReleasePool pool; 537 538 NSView *flushedView = static_cast<QCocoaWindow *>(flushedWindow->handle())->view(); 539 540 // If the backingstore is just flushed, without being painted to first, then we may 541 // end in a situation where the backingstore is flushed to a layer with a different 542 // scale factor than the one it was created for in beginPaint. This is the client's 543 // fault in not picking up the change in scale factor of the window and re-painting 544 // the backingstore accordingly. To smoothing things out, we warn about this situation, 545 // and change the layer's contentsScale to match the scale of the back buffer, so that 546 // we at least cover the whole layer. This is necessary since we set the view's 547 // contents placement policy to NSViewLayerContentsPlacementTopLeft, which means 548 // AppKit will not do any scaling on our behalf. 549 if (m_buffers.back()->devicePixelRatio() != flushedView.layer.contentsScale) { 550 qCWarning(lcQpaBackingStore) << "Back buffer dpr of" << m_buffers.back()->devicePixelRatio() 551 << "doesn't match" << flushedView.layer << "contents scale of" << flushedView.layer.contentsScale 552 << "- updating layer to match."; 553 flushedView.layer.contentsScale = m_buffers.back()->devicePixelRatio(); 554 } 555 556 const bool isSingleBuffered = window()->format().swapBehavior() == QSurfaceFormat::SingleBuffer; 557 558 id backBufferSurface = (__bridge id)m_buffers.back()->surface(); 559 if (!isSingleBuffered && flushedView.layer.contents == backBufferSurface) { 560 // We've managed to paint to the back buffer again before Core Animation had time 561 // to flush the transaction and persist the layer changes to the window server, or 562 // we've been asked to flush without painting anything. The layer already knows about 563 // the back buffer, and we don't need to re-apply it to pick up any possible surface 564 // changes, so bail out early. 565 qCInfo(lcQpaBackingStore).nospace() << "Skipping flush of " << flushedView 566 << ", layer already reflects back buffer"; 567 return; 568 } 569 570 // Trigger a new display cycle if there isn't one. This ensures that our layer updates 571 // are committed as part of a display-cycle instead of on the next runloop pass. This 572 // means CA won't try to throttle us if we flush too fast, and we'll coalesce our flush 573 // with other pending view and layer updates. 574 flushedView.window.viewsNeedDisplay = YES; 575 576 if (isSingleBuffered) { 577 // The private API [CALayer reloadValueForKeyPath:@"contents"] would be preferable, 578 // but barring any side effects or performance issues we opt for the hammer for now. 579 flushedView.layer.contents = nil; 580 } 581 582 qCInfo(lcQpaBackingStore) << "Flushing" << backBufferSurface 583 << "to" << flushedView.layer << "of" << flushedView; 584 585 flushedView.layer.contents = backBufferSurface; 586 587 // Since we may receive multiple flushes before a new frame is started, we do not 588 // swap any buffers just yet. Instead we check in the next beginPaint if the layer's 589 // surface is in use, and if so swap to an unused surface as the new back buffer. 590 591 // Note: Ideally CoreAnimation would mark a surface as in use the moment we assign 592 // it to a layer, but as that's not the case we may end up painting to the same back 593 // buffer once more if we are painting faster than CA can ship the surfaces over to 594 // the window server. 595} 596 597void QCALayerBackingStore::flushSubWindow(QWindow *subWindow) 598{ 599 qCInfo(lcQpaBackingStore) << "Flushing sub-window" << subWindow 600 << "via its own backingstore"; 601 602 auto &subWindowBackingStore = m_subWindowBackingstores[subWindow]; 603 if (!subWindowBackingStore) { 604 subWindowBackingStore.reset(new QCALayerBackingStore(subWindow)); 605 QObject::connect(subWindow, &QObject::destroyed, this, &QCALayerBackingStore::windowDestroyed); 606 subWindowBackingStore->m_clearSurfaceOnPaint = false; 607 } 608 609 auto subWindowSize = subWindow->size(); 610 static const auto kNoStaticContents = QRegion(); 611 subWindowBackingStore->resize(subWindowSize, kNoStaticContents); 612 613 auto subWindowLocalRect = QRect(QPoint(), subWindowSize); 614 subWindowBackingStore->beginPaint(subWindowLocalRect); 615 616 QPainter painter(subWindowBackingStore->m_buffers.back()->asImage()); 617 painter.setCompositionMode(QPainter::CompositionMode_Source); 618 619 NSView *backingStoreView = static_cast<QCocoaWindow *>(window()->handle())->view(); 620 NSView *flushedView = static_cast<QCocoaWindow *>(subWindow->handle())->view(); 621 auto subviewRect = [flushedView convertRect:flushedView.bounds toView:backingStoreView]; 622 auto scale = flushedView.layer.contentsScale; 623 subviewRect = CGRectApplyAffineTransform(subviewRect, CGAffineTransformMakeScale(scale, scale)); 624 625 m_buffers.back()->lock(QPlatformGraphicsBuffer::SWReadAccess); 626 const QImage *backingStoreImage = m_buffers.back()->asImage(); 627 painter.drawImage(subWindowLocalRect, *backingStoreImage, QRectF::fromCGRect(subviewRect)); 628 m_buffers.back()->unlock(); 629 630 painter.end(); 631 subWindowBackingStore->endPaint(); 632 subWindowBackingStore->flush(subWindow, subWindowLocalRect, QPoint()); 633 634 qCInfo(lcQpaBackingStore) << "Done flushing sub-window" << subWindow; 635} 636 637void QCALayerBackingStore::windowDestroyed(QObject *object) 638{ 639 auto *window = static_cast<QWindow*>(object); 640 qCInfo(lcQpaBackingStore) << "Removing backingstore for sub-window" << window; 641 m_subWindowBackingstores.erase(window); 642} 643 644#ifndef QT_NO_OPENGL 645void QCALayerBackingStore::composeAndFlush(QWindow *window, const QRegion ®ion, const QPoint &offset, 646 QPlatformTextureList *textures, bool translucentBackground) 647{ 648 if (!prepareForFlush()) 649 return; 650 651 QPlatformBackingStore::composeAndFlush(window, region, offset, textures, translucentBackground); 652} 653#endif 654 655QImage QCALayerBackingStore::toImage() const 656{ 657 if (!const_cast<QCALayerBackingStore*>(this)->prepareForFlush()) 658 return QImage(); 659 660 // We need to make a copy here, as the returned image could be used just 661 // for reading, in which case it won't detach, and then the underlying 662 // image data might change under the feet of the client when we re-use 663 // the buffer at a later point. 664 m_buffers.back()->lock(QPlatformGraphicsBuffer::SWReadAccess); 665 QImage imageCopy = m_buffers.back()->asImage()->copy(); 666 m_buffers.back()->unlock(); 667 return imageCopy; 668} 669 670void QCALayerBackingStore::backingPropertiesChanged() 671{ 672 // Ideally this would be plumbed from the platform layer to QtGui, and 673 // the QBackingStore would be recreated, but we don't have that code yet, 674 // so at least make sure we update our backingstore when the backing 675 // properties (color space e.g.) are changed. 676 677 Q_ASSERT(window()->handle()); 678 679 qCDebug(lcQpaBackingStore) << "Backing properties for" << window() << "did change"; 680 681 qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers"; 682 for (auto &buffer : m_buffers) { 683 if (buffer) 684 buffer->setColorSpace(colorSpace()); 685 } 686} 687 688QPlatformGraphicsBuffer *QCALayerBackingStore::graphicsBuffer() const 689{ 690 return m_buffers.back().get(); 691} 692 693bool QCALayerBackingStore::prepareForFlush() 694{ 695 if (!m_buffers.back()) { 696 qCWarning(lcQpaBackingStore) << "Tried to flush backingstore without painting to it first"; 697 return false; 698 } 699 700 // Update dirty state of buffers based on what was painted. The back buffer will be 701 // less dirty, since we painted to it, while other buffers will become more dirty. 702 // This allows us to minimize copies between front and back buffers on swap in the 703 // cases where the painted region overlaps with the previous frame (front buffer). 704 for (const auto &buffer : m_buffers) { 705 if (buffer == m_buffers.back()) 706 buffer->dirtyRegion -= m_paintedRegion; 707 else 708 buffer->dirtyRegion += m_paintedRegion; 709 } 710 711 // After painting, the back buffer is only guaranteed to have content for the painted 712 // region, and may still have dirty areas that need to be synced up with the front buffer, 713 // if we have one. We know that the front buffer is always up to date. 714 if (!m_buffers.back()->dirtyRegion.isEmpty() && m_buffers.front() != m_buffers.back()) { 715 QRegion preserveRegion = m_buffers.back()->dirtyRegion; 716 qCDebug(lcQpaBackingStore) << "Preserving" << preserveRegion << "from front to back buffer"; 717 718 m_buffers.front()->lock(QPlatformGraphicsBuffer::SWReadAccess); 719 const QImage *frontBuffer = m_buffers.front()->asImage(); 720 721 const QRect frontSurfaceBounds(QPoint(0, 0), m_buffers.front()->size()); 722 const qreal sourceDevicePixelRatio = frontBuffer->devicePixelRatio(); 723 724 m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); 725 QPainter painter(m_buffers.back()->asImage()); 726 painter.setCompositionMode(QPainter::CompositionMode_Source); 727 728 // Let painter operate in device pixels, to make it easier to compare coordinates 729 const qreal targetDevicePixelRatio = painter.device()->devicePixelRatio(); 730 painter.scale(1.0 / targetDevicePixelRatio, 1.0 / targetDevicePixelRatio); 731 732 for (const QRect &rect : preserveRegion) { 733 QRect sourceRect(rect.topLeft() * sourceDevicePixelRatio, rect.size() * sourceDevicePixelRatio); 734 QRect targetRect(rect.topLeft() * targetDevicePixelRatio, rect.size() * targetDevicePixelRatio); 735 736#ifdef QT_DEBUG 737 if (Q_UNLIKELY(!frontSurfaceBounds.contains(sourceRect.bottomRight()))) { 738 qCWarning(lcQpaBackingStore) << "Front buffer too small to preserve" 739 << QRegion(sourceRect).subtracted(frontSurfaceBounds); 740 } 741#endif 742 painter.drawImage(targetRect, *frontBuffer, sourceRect); 743 } 744 745 m_buffers.back()->unlock(); 746 m_buffers.front()->unlock(); 747 748 // The back buffer is now completely in sync, ready to be presented 749 m_buffers.back()->dirtyRegion = QRegion(); 750 } 751 752 // Prepare for another round of painting 753 m_paintedRegion = QRegion(); 754 755 return true; 756} 757 758// ---------------------------------------------------------------------------- 759 760QCALayerBackingStore::GraphicsBuffer::GraphicsBuffer(const QSize &size, qreal devicePixelRatio, 761 const QPixelFormat &format, QCFType<CGColorSpaceRef> colorSpace) 762 : QIOSurfaceGraphicsBuffer(size, format) 763 , dirtyRegion(0, 0, size.width() / devicePixelRatio, size.height() / devicePixelRatio) 764 , m_devicePixelRatio(devicePixelRatio) 765{ 766 setColorSpace(colorSpace); 767} 768 769QImage *QCALayerBackingStore::GraphicsBuffer::asImage() 770{ 771 if (m_image.isNull()) { 772 qCDebug(lcQpaBackingStore) << "Setting up paint device for" << this; 773 CFRetain(surface()); 774 m_image = QImage(data(), size().width(), size().height(), 775 bytesPerLine(), QImage::toImageFormat(format()), 776 QImageCleanupFunction(CFRelease), surface()); 777 m_image.setDevicePixelRatio(m_devicePixelRatio); 778 } 779 780 Q_ASSERT_X(m_image.constBits() == data(), "QCALayerBackingStore", 781 "IOSurfaces should have have a fixed location in memory once created"); 782 783 return &m_image; 784} 785 786#include "moc_qcocoabackingstore.cpp" 787 788QT_END_NAMESPACE 789