summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/cocoa/qcocoabackingstore.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoabackingstore.mm')
-rw-r--r--src/plugins/platforms/cocoa/qcocoabackingstore.mm663
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 &region, 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 &region)
@@ -392,7 +105,8 @@ void QCALayerBackingStore::beginPaint(const QRegion &region)
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 &region, 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 &region, const QPoint &offset)
@@ -510,14 +316,23 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion &region,
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 &region,
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 &region,
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 &region, const QPoint &offset,
- QPlatformTextureList *textures, bool translucentBackground)
+QPlatformBackingStore::FlushResult QCALayerBackingStore::rhiFlush(QWindow *window,
+ qreal sourceDevicePixelRatio,
+ const QRegion &region,
+ 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"