diff options
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoabackingstore.mm')
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoabackingstore.mm | 364 |
1 files changed, 236 insertions, 128 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 01787da1af..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,14 +17,15 @@ 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); } // ---------------------------------------------------------------------------- @@ -67,7 +33,9 @@ QCFType<CGColorSpaceRef> QCocoaBackingStore::colorSpace() const QCALayerBackingStore::QCALayerBackingStore(QWindow *window) : QCocoaBackingStore(window) { - qCDebug(lcQpaBackingStore) << "Creating QCALayerBackingStore for" << window; + qCDebug(lcQpaBackingStore) << "Creating QCALayerBackingStore for" << window + << "with" << window->format(); + m_buffers.resize(1); observeBackingPropertiesChanges(); @@ -105,12 +73,11 @@ bool QCALayerBackingStore::eventFilter(QObject *watched, QEvent *event) 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) @@ -138,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() @@ -146,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 │ @@ -228,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; } @@ -247,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) @@ -256,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(); @@ -287,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 @@ -315,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) @@ -372,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; + } + + finalizeBackBuffer(); - QPlatformBackingStore::composeAndFlush(window, region, offset, textures, translucentBackground); + 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 @@ -409,10 +479,11 @@ void QCALayerBackingStore::backingPropertiesChanged() qCDebug(lcQpaBackingStore) << "Backing properties for" << window() << "did change"; - qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers"; + 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); } } @@ -421,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(); } // ---------------------------------------------------------------------------- @@ -491,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()) { @@ -514,6 +622,6 @@ QImage *QCALayerBackingStore::GraphicsBuffer::asImage() return &m_image; } -#include "moc_qcocoabackingstore.cpp" - QT_END_NAMESPACE + +#include "moc_qcocoabackingstore.cpp" |