diff options
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoabackingstore.mm')
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoabackingstore.mm | 663 |
1 files changed, 263 insertions, 400 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 26cab9aa58..b211b5d02d 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the plugins of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <AppKit/AppKit.h> @@ -45,6 +9,7 @@ #include "qcocoahelpers.h" #include <QtCore/qmath.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/qpainter.h> #include <QuartzCore/CATransaction.h> @@ -52,319 +17,67 @@ QT_BEGIN_NAMESPACE QCocoaBackingStore::QCocoaBackingStore(QWindow *window) - : QRasterBackingStore(window) + : QPlatformBackingStore(window) { } QCFType<CGColorSpaceRef> QCocoaBackingStore::colorSpace() const { - NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view(); - return QCFType<CGColorSpaceRef>::constructFromGet(view.window.colorSpace.CGColorSpace); + const auto *platformWindow = static_cast<QCocoaWindow *>(window()->handle()); + const QNSView *view = qnsview_cast(platformWindow->view()); + return QCFType<CGColorSpaceRef>::constructFromGet(view.colorSpace.CGColorSpace); } // ---------------------------------------------------------------------------- -QNSWindowBackingStore::QNSWindowBackingStore(QWindow *window) +QCALayerBackingStore::QCALayerBackingStore(QWindow *window) : QCocoaBackingStore(window) { - // Choose an appropriate window depth based on the requested surface format. - // On deep color displays the default bit depth is 16-bit, so unless we need - // that level of precision we opt out of it (and the expensive RGB32 -> RGB64 - // conversions that come with it if our backingstore depth does not match). - - NSWindow *nsWindow = static_cast<QCocoaWindow *>(window->handle())->view().window; - auto colorSpaceName = NSColorSpaceFromDepth(nsWindow.depthLimit); - - static const int kDefaultBitDepth = 8; - auto surfaceFormat = window->requestedFormat(); - auto bitsPerSample = qMax(kDefaultBitDepth, qMax(surfaceFormat.redBufferSize(), - qMax(surfaceFormat.greenBufferSize(), surfaceFormat.blueBufferSize()))); - - // NSBestDepth does not seem to guarantee a window depth deep enough for the - // given bits per sample, even if documented as such. For example, requesting - // 10 bits per sample will not give us a 16-bit format, even if that's what's - // available. Work around this by manually bumping the bit depth. - bitsPerSample = !(bitsPerSample & (bitsPerSample - 1)) - ? bitsPerSample : qNextPowerOfTwo(bitsPerSample); - - auto bestDepth = NSBestDepth(colorSpaceName, bitsPerSample, 0, NO, nullptr); - - // Disable dynamic depth limit, otherwise our depth limit will be overwritten - // by AppKit if the window moves to a screen with a different depth. We call - // this before setting the depth limit, as the call will reset the depth to 0. - [nsWindow setDynamicDepthLimit:NO]; + qCDebug(lcQpaBackingStore) << "Creating QCALayerBackingStore for" << window + << "with" << window->format(); - qCDebug(lcQpaBackingStore) << "Using" << NSBitsPerSampleFromDepth(bestDepth) - << "bit window depth for" << nsWindow; + m_buffers.resize(1); - nsWindow.depthLimit = bestDepth; + observeBackingPropertiesChanges(); + window->installEventFilter(this); } -QNSWindowBackingStore::~QNSWindowBackingStore() +QCALayerBackingStore::~QCALayerBackingStore() { } -bool QNSWindowBackingStore::windowHasUnifiedToolbar() const +void QCALayerBackingStore::observeBackingPropertiesChanges() { Q_ASSERT(window()->handle()); - return static_cast<QCocoaWindow *>(window()->handle())->m_drawContentBorderGradient; -} - -QImage::Format QNSWindowBackingStore::format() const -{ - if (windowHasUnifiedToolbar()) - return QImage::Format_ARGB32_Premultiplied; - - return QRasterBackingStore::format(); -} - -void QNSWindowBackingStore::resize(const QSize &size, const QRegion &staticContents) -{ - qCDebug(lcQpaBackingStore) << "Resize requested to" << size; - QRasterBackingStore::resize(size, staticContents); - - // The window shadow rendered by AppKit is based on the shape/content of the - // NSWindow surface. Technically any flush of the backingstore can result in - // a potentially new shape of the window, and would need a shadow invalidation, - // but this is likely too expensive to do at every flush for the few cases where - // clients change the shape dynamically. One case where we do know that the shadow - // likely needs invalidation, if the window has partially transparent content, - // is after a resize, where AppKit's default shadow may be based on the previous - // window content. - QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window()->handle()); - if (cocoaWindow->isContentView() && !cocoaWindow->isOpaque()) - cocoaWindow->m_needsInvalidateShadow = true; -} - -/*! - Flushes the given \a region from the specified \a window onto the - screen. - - The \a window is the top level window represented by this backingstore, - or a non-transient child of that window. - - If the \a window is a child window, the \a region will be in child window - coordinates, and the \a offset will be the child window's offset in relation - to the backingstore's top level window. -*/ -void QNSWindowBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) -{ - if (m_image.isNull()) - return; - - // Use local pool so that any stale image references are cleaned up after flushing - QMacAutoReleasePool pool; - - const QWindow *topLevelWindow = this->window(); - - Q_ASSERT(topLevelWindow->handle() && window->handle()); - Q_ASSERT(!topLevelWindow->handle()->isForeignWindow() && !window->handle()->isForeignWindow()); - - QNSView *topLevelView = qnsview_cast(static_cast<QCocoaWindow *>(topLevelWindow->handle())->view()); - QNSView *view = qnsview_cast(static_cast<QCocoaWindow *>(window->handle())->view()); - - if (lcQpaBackingStore().isDebugEnabled()) { - QString targetViewDescription; - if (view != topLevelView) { - QDebug targetDebug(&targetViewDescription); - targetDebug << "onto" << topLevelView << "at" << offset; - } - qCDebug(lcQpaBackingStore) << "Flushing" << region << "of" << view << qPrintable(targetViewDescription); - } - - // Normally a NSView is drawn via drawRect, as part of the display cycle in the - // main runloop, via setNeedsDisplay and friends. AppKit will lock focus on each - // individual view, starting with the top level and then traversing any subviews, - // calling drawRect for each of them. This pull model results in expose events - // sent to Qt, which result in drawing to the backingstore and flushing it. - // Qt may also decide to paint and flush the backingstore via e.g. timers, - // or other events such as mouse events, in which case we're in a push model. - // If there is no focused view, it means we're in the latter case, and need - // to manually flush the NSWindow after drawing to its graphic context. - const bool drawingOutsideOfDisplayCycle = ![NSView focusView]; - - // We also need to ensure the flushed view has focus, so that the graphics - // context is set up correctly (coordinate system, clipping, etc). Outside - // of the normal display cycle there is no focused view, as explained above, - // so we have to handle it manually. There's also a corner case inside the - // normal display cycle due to way QWidgetRepaintManager composits native child - // widgets, where we'll get a flush of a native child during the drawRect of - // its parent/ancestor, and the parent/ancestor being the one locked by AppKit. - // In this case we also need to lock and unlock focus manually. - const bool shouldHandleViewLockManually = [NSView focusView] != view; - if (shouldHandleViewLockManually && !QT_IGNORE_DEPRECATIONS([view lockFocusIfCanDraw])) { - qWarning() << "failed to lock focus of" << view; - return; - } - - const qreal devicePixelRatio = m_image.devicePixelRatio(); - - // If the flushed window is a content view, and we're filling the drawn area - // completely, or it doesn't have a window background we need to preserve, - // we can get away with copying instead of blending the backing store. - QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()); - const NSCompositingOperation compositingOperation = cocoaWindow->isContentView() - && (cocoaWindow->isOpaque() || view.window.backgroundColor == NSColor.clearColor) - ? NSCompositingOperationCopy : NSCompositingOperationSourceOver; - -#ifdef QT_DEBUG - static bool debugBackingStoreFlush = [[NSUserDefaults standardUserDefaults] - boolForKey:@"QtCocoaDebugBackingStoreFlush"]; -#endif - - // ------------------------------------------------------------------------- - - // The current contexts is typically a NSWindowGraphicsContext, but can be - // NSBitmapGraphicsContext e.g. when debugging the view hierarchy in Xcode. - // If we need to distinguish things here in the future, we can use e.g. - // [NSGraphicsContext drawingToScreen], or the attributes of the context. - NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; - Q_ASSERT_X(graphicsContext, "QCocoaBackingStore", - "Focusing the view should give us a current graphics context"); - - // Tag backingstore image with color space based on the window. - // Note: This does not copy the underlying image data. - QCFType<CGImageRef> cgImage = CGImageCreateCopyWithColorSpace( - QCFType<CGImageRef>(m_image.toCGImage()), colorSpace()); - - // Create temporary image to use for blitting, without copying image data - NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease]; - - QRegion clippedRegion = region; - for (QWindow *w = window; w; w = w->parent()) { - if (!w->mask().isEmpty()) { - clippedRegion &= w == window ? w->mask() - : w->mask().translated(window->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)))); - } - } - - for (const QRect &viewLocalRect : clippedRegion) { - QPoint backingStoreOffset = viewLocalRect.topLeft() + offset; - QRect backingStoreRect(backingStoreOffset * devicePixelRatio, viewLocalRect.size() * devicePixelRatio); - if (graphicsContext.flipped) // Flip backingStoreRect to match graphics context - backingStoreRect.moveTop(m_image.height() - (backingStoreRect.y() + backingStoreRect.height())); - - CGRect viewRect = viewLocalRect.toCGRect(); - - [backingStoreImage drawInRect:viewRect fromRect:backingStoreRect.toCGRect() - operation:compositingOperation fraction:1.0 respectFlipped:YES hints:nil]; - -#ifdef QT_DEBUG - if (Q_UNLIKELY(debugBackingStoreFlush)) { - [[NSColor colorWithCalibratedRed:drand48() green:drand48() blue:drand48() alpha:0.3] set]; - [NSBezierPath fillRect:viewRect]; - - if (drawingOutsideOfDisplayCycle) { - [[[NSColor magentaColor] colorWithAlphaComponent:0.5] set]; - [NSBezierPath strokeLineFromPoint:viewLocalRect.topLeft().toCGPoint() - toPoint:viewLocalRect.bottomRight().toCGPoint()]; - } - } -#endif - } - - // ------------------------------------------------------------------------- - - if (shouldHandleViewLockManually) - QT_IGNORE_DEPRECATIONS([view unlockFocus]); - - if (drawingOutsideOfDisplayCycle) { - redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]); - QT_IGNORE_DEPRECATIONS([view.window flushWindow]); - } - - // Done flushing to NSWindow backingstore - - QCocoaWindow *topLevelCocoaWindow = static_cast<QCocoaWindow *>(topLevelWindow->handle()); - if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) { - qCDebug(lcQpaBackingStore) << "Invalidating window shadow for" << topLevelCocoaWindow; - [topLevelView.window invalidateShadow]; - topLevelCocoaWindow->m_needsInvalidateShadow = false; - } -} - -/* - When drawing outside of the display cycle, which Qt Widget does a lot, - we end up drawing over the NSThemeFrame, losing the rounded corners of - windows in the process. - - To work around this, until we've enabled updates via setNeedsDisplay and/or - enabled layer-backed views, we ask the NSWindow to redraw the bottom corners - if they intersect with the flushed region. - - This is the same logic used internally by e.g [NSView displayIfNeeded], - [NSRulerView _scrollToMatchContentView], and [NSClipView _immediateScrollToPoint:], - as well as the workaround used by WebKit to fix a similar bug: - - https://trac.webkit.org/changeset/85376/webkit -*/ -void QNSWindowBackingStore::redrawRoundedBottomCorners(CGRect windowRect) const -{ -#if !defined(QT_APPLE_NO_PRIVATE_APIS) - Q_ASSERT(this->window()->handle()); - NSWindow *window = static_cast<QCocoaWindow *>(this->window()->handle())->nativeWindow(); - - static SEL intersectBottomCornersWithRect = NSSelectorFromString( - [NSString stringWithFormat:@"_%s%s:", "intersectBottomCorners", "WithRect"]); - if (NSMethodSignature *signature = [window methodSignatureForSelector:intersectBottomCornersWithRect]) { - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - invocation.target = window; - invocation.selector = intersectBottomCornersWithRect; - [invocation setArgument:&windowRect atIndex:2]; - [invocation invoke]; - - NSRect cornerOverlap = NSZeroRect; - [invocation getReturnValue:&cornerOverlap]; - if (!NSIsEmptyRect(cornerOverlap)) { - static SEL maskRoundedBottomCorners = NSSelectorFromString( - [NSString stringWithFormat:@"_%s%s:", "maskRounded", "BottomCorners"]); - if ((signature = [window methodSignatureForSelector:maskRoundedBottomCorners])) { - invocation = [NSInvocation invocationWithMethodSignature:signature]; - invocation.target = window; - invocation.selector = maskRoundedBottomCorners; - [invocation setArgument:&cornerOverlap atIndex:2]; - [invocation invoke]; - } - } - } -#else - Q_UNUSED(windowRect); -#endif -} - -// ---------------------------------------------------------------------------- - -QCALayerBackingStore::QCALayerBackingStore(QWindow *window) - : QCocoaBackingStore(window) -{ - qCDebug(lcQpaBackingStore) << "Creating QCALayerBackingStore for" << window; - m_buffers.resize(1); - - // Ideally this would be plumbed from the platform layer to QtGui, and - // the QBackingStore would be recreated, but we don't have that code yet, - // so at least make sure we update our backingstore when the backing - // properties (color space e.g.) are changed. - NSView *view = static_cast<QCocoaWindow *>(window->handle())->view(); + NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view(); m_backingPropertiesObserver = QMacNotificationObserver(view.window, NSWindowDidChangeBackingPropertiesNotification, [this]() { - qCDebug(lcQpaBackingStore) << "Backing properties for" - << this->window() << "did change"; backingPropertiesChanged(); }); } -QCALayerBackingStore::~QCALayerBackingStore() +bool QCALayerBackingStore::eventFilter(QObject *watched, QEvent *event) { + Q_ASSERT(watched == window()); + + if (event->type() == QEvent::PlatformSurface) { + auto *surfaceEvent = static_cast<QPlatformSurfaceEvent*>(event); + if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) + observeBackingPropertiesChanges(); + else + m_backingPropertiesObserver = QMacNotificationObserver(); + } + + return false; } void QCALayerBackingStore::resize(const QSize &size, const QRegion &staticContents) { - qCDebug(lcQpaBackingStore) << "Resize requested to" << size; - - if (!staticContents.isNull()) - qCWarning(lcQpaBackingStore) << "QCALayerBackingStore does not support static contents"; + qCDebug(lcQpaBackingStore) << "Resize requested to" << size + << "with static contents" << staticContents; m_requestedSize = size; + m_staticContents = staticContents; } void QCALayerBackingStore::beginPaint(const QRegion ®ion) @@ -392,7 +105,8 @@ void QCALayerBackingStore::beginPaint(const QRegion ®ion) painter.fillRect(rect, Qt::transparent); } - m_paintedRegion += region; + // We assume the client is going to paint the entire region + updateDirtyStates(region); } void QCALayerBackingStore::ensureBackBuffer() @@ -400,13 +114,6 @@ void QCALayerBackingStore::ensureBackBuffer() if (window()->format().swapBehavior() == QSurfaceFormat::SingleBuffer) return; - // The current back buffer may have been assigned to a layer in a previous flush, - // but we deferred the swap. Do it now if the surface has been picked up by CA. - if (m_buffers.back() && m_buffers.back()->isInUse() && m_buffers.back() != m_buffers.front()) { - qCInfo(lcQpaBackingStore) << "Back buffer has been picked up by CA, swapping to front"; - std::swap(m_buffers.back(), m_buffers.front()); - } - if (Q_UNLIKELY(lcQpaBackingStore().isDebugEnabled())) { // ┌───────┬───────┬───────┬─────┬──────┐ // │ front ┊ spare ┊ spare ┊ ... ┊ back │ @@ -482,11 +189,50 @@ bool QCALayerBackingStore::recreateBackBufferIfNeeded() } #endif - qCInfo(lcQpaBackingStore) << "Creating surface of" << requestedBufferSize - << "based on requested" << m_requestedSize << "and dpr =" << devicePixelRatio; + qCInfo(lcQpaBackingStore)<< "Creating surface of" << requestedBufferSize + << "for" << window() << "based on requested" << m_requestedSize + << "dpr =" << devicePixelRatio << "and color space" << colorSpace(); static auto pixelFormat = QImage::toPixelFormat(QImage::Format_ARGB32_Premultiplied); - m_buffers.back().reset(new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace())); + auto *newBackBuffer = new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace()); + + if (!m_staticContents.isEmpty() && m_buffers.back()) { + // We implicitly support static backingstore content as a result of + // finalizing the back buffer on flush, where we copy any non-painted + // areas from the front buffer. But there is no guarantee that a resize + // will always come after a flush, where we have a pristine front buffer + // to copy from. It may come after a few begin/endPaints, where the back + // buffer then contains (part of) the latest state. We also have the case + // of single-buffered backingstore, where the front and back buffer is + // the same, which means we must do the copy from the old back buffer + // to the newly resized buffer now, before we replace it below. + + // If the back buffer has been partially filled already, we need to + // copy parts of the static content from that. The rest we copy from + // the front buffer. + const QRegion backBufferRegion = m_staticContents - m_buffers.back()->dirtyRegion; + const QRegion frontBufferRegion = m_staticContents - backBufferRegion; + + qCInfo(lcQpaBackingStore) << "Preserving static content" << backBufferRegion + << "from back buffer, and" << frontBufferRegion << "from front buffer"; + + newBackBuffer->lock(QPlatformGraphicsBuffer::SWWriteAccess); + blitBuffer(m_buffers.back().get(), backBufferRegion, newBackBuffer); + Q_ASSERT(frontBufferRegion.isEmpty() || m_buffers.front()); + blitBuffer(m_buffers.front().get(), frontBufferRegion, newBackBuffer); + newBackBuffer->unlock(); + + // The new back buffer now is valid for the static contents region. + // We don't need to maintain the static contents region for resizes + // of any other buffers in the swap chain, as these will finalize + // their content on flush from the buffer we just filled, and we + // don't need to mark them dirty for the area we just filled, as + // new buffers are fully dirty when created. + newBackBuffer->dirtyRegion -= m_staticContents; + m_staticContents = {}; + } + + m_buffers.back().reset(newBackBuffer); return true; } @@ -501,8 +247,68 @@ QPaintDevice *QCALayerBackingStore::paintDevice() void QCALayerBackingStore::endPaint() { - qCInfo(lcQpaBackingStore) << "Paint ended with painted region" << m_paintedRegion; + qCInfo(lcQpaBackingStore) << "Paint ended. Back buffer valid region is now" << m_buffers.back()->validRegion(); + m_buffers.back()->unlock(); + + // Since we can have multiple begin/endPaint rounds before a flush + // we defer finalizing the back buffer until its content is needed. +} + +bool QCALayerBackingStore::scroll(const QRegion ®ion, int dx, int dy) +{ + if (!m_buffers.back()) { + qCInfo(lcQpaBackingStore) << "Scroll requested with no back buffer. Ignoring."; + return false; + } + + const QPoint scrollDelta(dx, dy); + qCInfo(lcQpaBackingStore) << "Scrolling" << region << "by" << scrollDelta; + + ensureBackBuffer(); + recreateBackBufferIfNeeded(); + + const QRegion inPlaceRegion = region - m_buffers.back()->dirtyRegion; + const QRegion frontBufferRegion = region - inPlaceRegion; + + QMacAutoReleasePool pool; + + m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); + + if (!inPlaceRegion.isEmpty()) { + // We have to scroll everything in one go, instead of scrolling the + // individual rects of the region, as otherwise we may end up reading + // already overwritten (scrolled) pixels. + const QRect inPlaceBoundingRect = inPlaceRegion.boundingRect(); + + qCDebug(lcQpaBackingStore) << "Scrolling" << inPlaceBoundingRect << "in place"; + QImage *backBufferImage = m_buffers.back()->asImage(); + const qreal devicePixelRatio = backBufferImage->devicePixelRatio(); + const QPoint devicePixelDelta = scrollDelta * devicePixelRatio; + + extern void qt_scrollRectInImage(QImage &, const QRect &, const QPoint &); + + qt_scrollRectInImage(*backBufferImage, + QRect(inPlaceBoundingRect.topLeft() * devicePixelRatio, + inPlaceBoundingRect.size() * devicePixelRatio), + devicePixelDelta); + } + + if (!frontBufferRegion.isEmpty()) { + qCDebug(lcQpaBackingStore) << "Scrolling" << frontBufferRegion << "by copying from front buffer"; + blitBuffer(m_buffers.front().get(), frontBufferRegion, m_buffers.back().get(), scrollDelta); + } + m_buffers.back()->unlock(); + + // Mark the target region as filled. Note: We do not mark the source region + // as dirty, even though the content has conceptually been "moved", as that + // would complicate things when preserving from the front buffer. This matches + // the behavior of other backingstore implementations using qt_scrollRectInImage. + updateDirtyStates(region.translated(scrollDelta)); + + qCInfo(lcQpaBackingStore) << "Scroll ended. Back buffer valid region is now" << m_buffers.back()->validRegion(); + + return true; } void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion ®ion, const QPoint &offset) @@ -510,14 +316,23 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion ®ion, Q_UNUSED(region); Q_UNUSED(offset); - if (!prepareForFlush()) + if (!m_buffers.back()) { + qCWarning(lcQpaBackingStore) << "Tried to flush backingstore without painting to it first"; return; + } + + finalizeBackBuffer(); if (flushedWindow != window()) { flushSubWindow(flushedWindow); return; } + if (m_buffers.front()->isInUse() && !m_buffers.front()->isDirty()) { + qCInfo(lcQpaBackingStore) << "Asked to flush, but front buffer is up to date. Ignoring."; + return; + } + QMacAutoReleasePool pool; NSView *flushedView = static_cast<QCocoaWindow *>(flushedWindow->handle())->view(); @@ -541,16 +356,6 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion ®ion, const bool isSingleBuffered = window()->format().swapBehavior() == QSurfaceFormat::SingleBuffer; id backBufferSurface = (__bridge id)m_buffers.back()->surface(); - if (!isSingleBuffered && flushedView.layer.contents == backBufferSurface) { - // We've managed to paint to the back buffer again before Core Animation had time - // to flush the transaction and persist the layer changes to the window server, or - // we've been asked to flush without painting anything. The layer already knows about - // the back buffer, and we don't need to re-apply it to pick up any possible surface - // changes, so bail out early. - qCInfo(lcQpaBackingStore).nospace() << "Skipping flush of " << flushedView - << ", layer already reflects back buffer"; - return; - } // Trigger a new display cycle if there isn't one. This ensures that our layer updates // are committed as part of a display-cycle instead of on the next runloop pass. This @@ -569,14 +374,17 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion ®ion, flushedView.layer.contents = backBufferSurface; - // Since we may receive multiple flushes before a new frame is started, we do not - // swap any buffers just yet. Instead we check in the next beginPaint if the layer's - // surface is in use, and if so swap to an unused surface as the new back buffer. + if (!isSingleBuffered) { + // Mark the surface as in use, so that we don't end up rendering + // to it while it's assigned to a layer. + IOSurfaceIncrementUseCount(m_buffers.back()->surface()); - // Note: Ideally CoreAnimation would mark a surface as in use the moment we assign - // it to a layer, but as that's not the case we may end up painting to the same back - // buffer once more if we are painting faster than CA can ship the surfaces over to - // the window server. + if (m_buffers.back() != m_buffers.front()) { + qCInfo(lcQpaBackingStore) << "Swapping back buffer to front"; + std::swap(m_buffers.back(), m_buffers.front()); + IOSurfaceDecrementUseCount(m_buffers.back()->surface()); + } + } } void QCALayerBackingStore::flushSubWindow(QWindow *subWindow) @@ -626,22 +434,30 @@ void QCALayerBackingStore::windowDestroyed(QObject *object) m_subWindowBackingstores.erase(window); } -#ifndef QT_NO_OPENGL -void QCALayerBackingStore::composeAndFlush(QWindow *window, const QRegion ®ion, const QPoint &offset, - QPlatformTextureList *textures, bool translucentBackground) +QPlatformBackingStore::FlushResult QCALayerBackingStore::rhiFlush(QWindow *window, + qreal sourceDevicePixelRatio, + const QRegion ®ion, + const QPoint &offset, + QPlatformTextureList *textures, + bool translucentBackground) { - if (!prepareForFlush()) - return; + if (!m_buffers.back()) { + qCWarning(lcQpaBackingStore) << "Tried to flush backingstore without painting to it first"; + return FlushFailed; + } - QPlatformBackingStore::composeAndFlush(window, region, offset, textures, translucentBackground); + finalizeBackBuffer(); + + return QPlatformBackingStore::rhiFlush(window, sourceDevicePixelRatio, region, offset, textures, translucentBackground); } -#endif QImage QCALayerBackingStore::toImage() const { - if (!const_cast<QCALayerBackingStore*>(this)->prepareForFlush()) + if (!m_buffers.back()) return QImage(); + const_cast<QCALayerBackingStore*>(this)->finalizeBackBuffer(); + // We need to make a copy here, as the returned image could be used just // for reading, in which case it won't detach, and then the underlying // image data might change under the feet of the client when we re-use @@ -654,10 +470,20 @@ QImage QCALayerBackingStore::toImage() const void QCALayerBackingStore::backingPropertiesChanged() { - qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers"; + // Ideally this would be plumbed from the platform layer to QtGui, and + // the QBackingStore would be recreated, but we don't have that code yet, + // so at least make sure we update our backingstore when the backing + // properties (color space e.g.) are changed. + + Q_ASSERT(window()->handle()); + + qCDebug(lcQpaBackingStore) << "Backing properties for" << window() << "did change"; + + const auto newColorSpace = colorSpace(); + qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers to" << newColorSpace; for (auto &buffer : m_buffers) { if (buffer) - buffer->setColorSpace(colorSpace()); + buffer->setColorSpace(newColorSpace); } } @@ -666,69 +492,99 @@ QPlatformGraphicsBuffer *QCALayerBackingStore::graphicsBuffer() const return m_buffers.back().get(); } -bool QCALayerBackingStore::prepareForFlush() +void QCALayerBackingStore::updateDirtyStates(const QRegion &paintedRegion) { - if (!m_buffers.back()) { - qCWarning(lcQpaBackingStore) << "Tried to flush backingstore without painting to it first"; - return false; - } - // Update dirty state of buffers based on what was painted. The back buffer will be // less dirty, since we painted to it, while other buffers will become more dirty. // This allows us to minimize copies between front and back buffers on swap in the // cases where the painted region overlaps with the previous frame (front buffer). for (const auto &buffer : m_buffers) { if (buffer == m_buffers.back()) - buffer->dirtyRegion -= m_paintedRegion; + buffer->dirtyRegion -= paintedRegion; else - buffer->dirtyRegion += m_paintedRegion; + buffer->dirtyRegion += paintedRegion; } +} +void QCALayerBackingStore::finalizeBackBuffer() +{ // After painting, the back buffer is only guaranteed to have content for the painted // region, and may still have dirty areas that need to be synced up with the front buffer, // if we have one. We know that the front buffer is always up to date. - if (!m_buffers.back()->dirtyRegion.isEmpty() && m_buffers.front() != m_buffers.back()) { - QRegion preserveRegion = m_buffers.back()->dirtyRegion; - qCDebug(lcQpaBackingStore) << "Preserving" << preserveRegion << "from front to back buffer"; - m_buffers.front()->lock(QPlatformGraphicsBuffer::SWReadAccess); - const QImage *frontBuffer = m_buffers.front()->asImage(); + if (!m_buffers.back()->isDirty()) + return; - const QRect frontSurfaceBounds(QPoint(0, 0), m_buffers.front()->size()); - const qreal sourceDevicePixelRatio = frontBuffer->devicePixelRatio(); + qCDebug(lcQpaBackingStore) << "Finalizing back buffer with dirty region" << m_buffers.back()->dirtyRegion; + if (m_buffers.back() != m_buffers.front()) { m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); - QPainter painter(m_buffers.back()->asImage()); - painter.setCompositionMode(QPainter::CompositionMode_Source); + blitBuffer(m_buffers.front().get(), m_buffers.back()->dirtyRegion, m_buffers.back().get()); + m_buffers.back()->unlock(); + } else { + qCDebug(lcQpaBackingStore) << "Front and back buffer is the same. Can not finalize back buffer."; + } - // Let painter operate in device pixels, to make it easier to compare coordinates - const qreal targetDevicePixelRatio = painter.device()->devicePixelRatio(); - painter.scale(1.0 / targetDevicePixelRatio, 1.0 / targetDevicePixelRatio); + // The back buffer is now completely in sync, ready to be presented + m_buffers.back()->dirtyRegion = QRegion(); +} - for (const QRect &rect : preserveRegion) { - QRect sourceRect(rect.topLeft() * sourceDevicePixelRatio, rect.size() * sourceDevicePixelRatio); - QRect targetRect(rect.topLeft() * targetDevicePixelRatio, rect.size() * targetDevicePixelRatio); +/* + \internal -#ifdef QT_DEBUG - if (Q_UNLIKELY(!frontSurfaceBounds.contains(sourceRect.bottomRight()))) { - qCWarning(lcQpaBackingStore) << "Front buffer too small to preserve" - << QRegion(sourceRect).subtracted(frontSurfaceBounds); - } -#endif - painter.drawImage(targetRect, *frontBuffer, sourceRect); - } + Blits \a sourceRegion from \a sourceBuffer to \a destinationBuffer, + at offset \a destinationOffset. - m_buffers.back()->unlock(); - m_buffers.front()->unlock(); + The source buffer is automatically locked for read only access + during the blit. - // The back buffer is now completely in sync, ready to be presented - m_buffers.back()->dirtyRegion = QRegion(); - } + The destination buffer has to be locked for write access by the + caller. +*/ + +void QCALayerBackingStore::blitBuffer(GraphicsBuffer *sourceBuffer, const QRegion &sourceRegion, + GraphicsBuffer *destinationBuffer, const QPoint &destinationOffset) +{ + Q_ASSERT(sourceBuffer && destinationBuffer); + Q_ASSERT(sourceBuffer != destinationBuffer); - // Prepare for another round of painting - m_paintedRegion = QRegion(); + if (sourceRegion.isEmpty()) + return; - return true; + qCDebug(lcQpaBackingStore) << "Blitting" << sourceRegion << "of" << sourceBuffer + << "to" << sourceRegion.translated(destinationOffset) << "of" << destinationBuffer; + + Q_ASSERT(destinationBuffer->isLocked() == QPlatformGraphicsBuffer::SWWriteAccess); + + sourceBuffer->lock(QPlatformGraphicsBuffer::SWReadAccess); + const QImage *sourceImage = sourceBuffer->asImage(); + + const QRect sourceBufferBounds(QPoint(0, 0), sourceBuffer->size()); + const qreal sourceDevicePixelRatio = sourceImage->devicePixelRatio(); + + QPainter painter(destinationBuffer->asImage()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + + // Let painter operate in device pixels, to make it easier to compare coordinates + const qreal destinationDevicePixelRatio = painter.device()->devicePixelRatio(); + painter.scale(1.0 / destinationDevicePixelRatio, 1.0 / destinationDevicePixelRatio); + + for (const QRect &rect : sourceRegion) { + QRect sourceRect(rect.topLeft() * sourceDevicePixelRatio, + rect.size() * sourceDevicePixelRatio); + QRect destinationRect((rect.topLeft() + destinationOffset) * destinationDevicePixelRatio, + rect.size() * destinationDevicePixelRatio); + +#ifdef QT_DEBUG + if (Q_UNLIKELY(!sourceBufferBounds.contains(sourceRect.bottomRight()))) { + qCWarning(lcQpaBackingStore) << "Source buffer of size" << sourceBuffer->size() + << "is too small to blit" << sourceRect; + } +#endif + painter.drawImage(destinationRect, *sourceImage, sourceRect); + } + + sourceBuffer->unlock(); } // ---------------------------------------------------------------------------- @@ -736,12 +592,19 @@ bool QCALayerBackingStore::prepareForFlush() QCALayerBackingStore::GraphicsBuffer::GraphicsBuffer(const QSize &size, qreal devicePixelRatio, const QPixelFormat &format, QCFType<CGColorSpaceRef> colorSpace) : QIOSurfaceGraphicsBuffer(size, format) - , dirtyRegion(0, 0, size.width() / devicePixelRatio, size.height() / devicePixelRatio) + , dirtyRegion(QRect(QPoint(0, 0), size / devicePixelRatio)) , m_devicePixelRatio(devicePixelRatio) { setColorSpace(colorSpace); } +QRegion QCALayerBackingStore::GraphicsBuffer::validRegion() const +{ + + QRegion fullRegion = QRect(QPoint(0, 0), size() / m_devicePixelRatio); + return fullRegion - dirtyRegion; +} + QImage *QCALayerBackingStore::GraphicsBuffer::asImage() { if (m_image.isNull()) { @@ -759,6 +622,6 @@ QImage *QCALayerBackingStore::GraphicsBuffer::asImage() return &m_image; } -#include "moc_qcocoabackingstore.cpp" - QT_END_NAMESPACE + +#include "moc_qcocoabackingstore.cpp" |