summaryrefslogtreecommitdiffstats
path: root/src/plugins
diff options
context:
space:
mode:
authorTor Arne Vestbø <tor.arne.vestbo@qt.io>2019-01-30 13:14:38 +0100
committerTor Arne Vestbø <tor.arne.vestbo@qt.io>2019-03-14 21:51:27 +0000
commit77a4915bf900aac0c96260018c0a4ccfbdd7c094 (patch)
tree07398f665b0340b4d1ed9596512d23a95c8d05bd /src/plugins
parent097bf6fdd234ca7e5707610edfd3c804b3ba2a52 (diff)
macOS: Add IOSurface based backingstore for layer-backed views
The naïve approach used for layer-backing in the past caused a detach of the backingstore QImage on each beginPaint, since the image was assigned to the layer via a CGImageRef that participated in the QImage implicit sharing (and had to, so we couldn't get around that). We now use IOSurfaces, wrapped in a QPlatformGraphicsBuffer abstraction. The surfaces can be assigned to the layer's content the same way images could, but allows us to reason more closely about whether or a buffer is in use, and increases the chance that we will have a zero-copy path to the screen. Unless the window has requested a surface format with single buffering we use a dynamic swap chain of buffers. In most situations there will be two buffers in play, one assigned to the layer and one ready to paint to, but during resize and some other situations the buffers will grow temporarily to accommodate the increased back-pressure. Since QBackingStore is documented as having single-buffer behavior, we take care to persist content between the buffers before every swap. By doing this before swapping, instead of before each paint, we can avoid preserving areas that will be painted to anyways, and will in many situations (such as blinking cursors e.g.) end up not persisting anything. The RasterGL surface case is handled by reading out the buffer data and doing a manual texture upload. In the future we can support direct texture access via CGLTexImageIOSurface2D, but this requires QPlatformBackingStore::composeAndFlush to learn how to support other targets than GL_TEXTURE_2D, as CGLTexImageIOSurface2D only works with GL_TEXTURE_RECTANGLE_ARB targets. Fixes: QTBUG-48763 Fixes: QTBUG-72360 Fixes: QTBUG-71162 Change-Id: Ica12f69b244e54d0fd31c929730d15657c286af8 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/platforms/cocoa/cocoa.pro4
-rw-r--r--src/plugins/platforms/cocoa/qcocoabackingstore.h52
-rw-r--r--src/plugins/platforms/cocoa/qcocoabackingstore.mm543
-rw-r--r--src/plugins/platforms/cocoa/qcocoaintegration.mm11
-rw-r--r--src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h77
-rw-r--r--src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.mm188
-rw-r--r--src/plugins/platforms/cocoa/qnsview_drawing.mm7
7 files changed, 769 insertions, 113 deletions
diff --git a/src/plugins/platforms/cocoa/cocoa.pro b/src/plugins/platforms/cocoa/cocoa.pro
index 8d65cf328f..083b7c1655 100644
--- a/src/plugins/platforms/cocoa/cocoa.pro
+++ b/src/plugins/platforms/cocoa/cocoa.pro
@@ -33,6 +33,7 @@ SOURCES += main.mm \
qcocoaintrospection.mm \
qcocoakeymapper.mm \
qcocoamimetypes.mm \
+ qiosurfacegraphicsbuffer.mm \
messages.cpp
HEADERS += qcocoaintegration.h \
@@ -67,6 +68,7 @@ HEADERS += qcocoaintegration.h \
qcocoaintrospection.h \
qcocoakeymapper.h \
messages.h \
+ qiosurfacegraphicsbuffer.h \
qcocoamimetypes.h
qtConfig(opengl.*) {
@@ -81,7 +83,7 @@ qtConfig(vulkan) {
RESOURCES += qcocoaresources.qrc
-LIBS += -framework AppKit -framework CoreServices -framework Carbon -framework IOKit -framework QuartzCore -framework CoreVideo -framework Metal -lcups
+LIBS += -framework AppKit -framework CoreServices -framework Carbon -framework IOKit -framework QuartzCore -framework CoreVideo -framework Metal -framework IOSurface -lcups
QT += \
core-private gui-private \
diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.h b/src/plugins/platforms/cocoa/qcocoabackingstore.h
index b4cd506513..508f24d578 100644
--- a/src/plugins/platforms/cocoa/qcocoabackingstore.h
+++ b/src/plugins/platforms/cocoa/qcocoabackingstore.h
@@ -44,13 +44,16 @@
#include <private/qcore_mac_p.h>
+#include <QScopedPointer>
+#include "qiosurfacegraphicsbuffer.h"
+
QT_BEGIN_NAMESPACE
-class QCocoaBackingStore : public QRasterBackingStore
+class QNSWindowBackingStore : public QRasterBackingStore
{
public:
- QCocoaBackingStore(QWindow *window);
- ~QCocoaBackingStore();
+ QNSWindowBackingStore(QWindow *window);
+ ~QNSWindowBackingStore();
void flush(QWindow *, const QRegion &, const QPoint &) override;
@@ -60,6 +63,49 @@ private:
void redrawRoundedBottomCorners(CGRect) const;
};
+class QCALayerBackingStore : public QPlatformBackingStore
+{
+public:
+ QCALayerBackingStore(QWindow *window);
+ ~QCALayerBackingStore();
+
+ void resize(const QSize &size, const QRegion &staticContents) override;
+
+ void beginPaint(const QRegion &region) override;
+ QPaintDevice *paintDevice() override;
+ void endPaint() override;
+
+ void flush(QWindow *, const QRegion &, const QPoint &) override;
+ void composeAndFlush(QWindow *window, const QRegion &region, const QPoint &offset,
+ QPlatformTextureList *textures, bool translucentBackground) override;
+
+ QPlatformGraphicsBuffer *graphicsBuffer() const override;
+
+private:
+ QSize m_requestedSize;
+ QRegion m_paintedRegion;
+
+ class GraphicsBuffer : public QIOSurfaceGraphicsBuffer
+ {
+ public:
+ GraphicsBuffer(const QSize &size, qreal devicePixelRatio,
+ const QPixelFormat &format, QCFType<CGColorSpaceRef> colorSpace);
+
+ QRegion dirtyRegion; // In unscaled coordinates
+ QImage *asImage();
+
+ private:
+ qreal m_devicePixelRatio;
+ QImage m_image;
+ };
+
+ void ensureBackBuffer();
+ bool recreateBackBufferIfNeeded();
+ bool prepareForFlush();
+
+ std::list<std::unique_ptr<GraphicsBuffer>> m_buffers;
+};
+
QT_END_NAMESPACE
#endif
diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm
index 81a0a7d040..8e4e928bc5 100644
--- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm
+++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm
@@ -42,24 +42,28 @@
#include "qcocoawindow.h"
#include "qcocoahelpers.h"
+#include <QtCore/qmath.h>
+
+#include <QuartzCore/CATransaction.h>
+
QT_BEGIN_NAMESPACE
-QCocoaBackingStore::QCocoaBackingStore(QWindow *window)
+QNSWindowBackingStore::QNSWindowBackingStore(QWindow *window)
: QRasterBackingStore(window)
{
}
-QCocoaBackingStore::~QCocoaBackingStore()
+QNSWindowBackingStore::~QNSWindowBackingStore()
{
}
-bool QCocoaBackingStore::windowHasUnifiedToolbar() const
+bool QNSWindowBackingStore::windowHasUnifiedToolbar() const
{
Q_ASSERT(window()->handle());
return static_cast<QCocoaWindow *>(window()->handle())->m_drawContentBorderGradient;
}
-QImage::Format QCocoaBackingStore::format() const
+QImage::Format QNSWindowBackingStore::format() const
{
if (windowHasUnifiedToolbar())
return QImage::Format_ARGB32_Premultiplied;
@@ -78,7 +82,7 @@ QImage::Format QCocoaBackingStore::format() const
coordinates, and the \a offset will be the child window's offset in relation
to the backingstore's top level window.
*/
-void QCocoaBackingStore::flush(QWindow *window, const QRegion &region, const QPoint &offset)
+void QNSWindowBackingStore::flush(QWindow *window, const QRegion &region, const QPoint &offset)
{
if (m_image.isNull())
return;
@@ -103,131 +107,113 @@ void QCocoaBackingStore::flush(QWindow *window, const QRegion &region, const QPo
qCDebug(lcQpaBackingStore) << "Flushing" << region << "of" << view << qPrintable(targetViewDescription);
}
- // Prevent potentially costly color conversion by assigning the display color space
- // to the backingstore image. This does not copy the underlying image data.
- CGColorSpaceRef displayColorSpace = view.window.screen.colorSpace.CGColorSpace;
- QCFType<CGImageRef> cgImage = CGImageCreateCopyWithColorSpace(
- QCFType<CGImageRef>(m_image.toCGImage()), displayColorSpace);
-
- if (view.layer) {
- // In layer-backed mode, locking focus on a view does not give the right
- // view transformation, and doesn't give us a graphics context to render
- // via when drawing outside of the display cycle. Instead we tell AppKit
- // that we want to update the layer's content, via [NSView wantsUpdateLayer],
- // which result in AppKit not creating a backingstore for each layer, and
- // we then directly set the layer's backingstore (content) to our backingstore,
- // masked to the part of the subview that is relevant.
- // FIXME: Figure out if there's a way to do partial updates
- view.layer.contents = (__bridge id)static_cast<CGImageRef>(cgImage);
- if (view != topLevelView) {
- const CGSize topLevelSize = topLevelView.bounds.size;
- view.layer.contentsRect = CGRectApplyAffineTransform(
- [view convertRect:view.bounds toView:topLevelView],
- // The contentsRect is in unit coordinate system
- CGAffineTransformMakeScale(1.0 / topLevelSize.width, 1.0 / topLevelSize.height));
- }
- } else {
- // 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 QWidgetBackingStore 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 && ![view lockFocusIfCanDraw]) {
- qWarning() << "failed to lock focus of" << view;
- return;
- }
+ // 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 QWidgetBackingStore 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 && ![view lockFocusIfCanDraw]) {
+ qWarning() << "failed to lock focus of" << view;
+ return;
+ }
- const qreal devicePixelRatio = m_image.devicePixelRatio();
+ 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;
+ // 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"];
+ 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");
+ // 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");
- // Create temporary image to use for blitting, without copying image data
- NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease];
+ // Prevent potentially costly color conversion by assigning the display color space
+ // to the backingstore image. This does not copy the underlying image data.
+ CGColorSpaceRef displayColorSpace = view.window.screen.colorSpace.CGColorSpace;
+ QCFType<CGImageRef> cgImage = CGImageCreateCopyWithColorSpace(
+ QCFType<CGImageRef>(m_image.toCGImage()), displayColorSpace);
- 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))));
- }
+ // 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()));
+ 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();
+ CGRect viewRect = viewLocalRect.toCGRect();
- if (windowHasUnifiedToolbar())
- NSDrawWindowBackground(viewRect);
+ if (windowHasUnifiedToolbar())
+ NSDrawWindowBackground(viewRect);
- [backingStoreImage drawInRect:viewRect fromRect:backingStoreRect.toCGRect()
- operation:compositingOperation fraction:1.0 respectFlipped:YES hints:nil];
+ [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()];
- }
+ 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
}
+#endif
+ }
- // -------------------------------------------------------------------------
+ // -------------------------------------------------------------------------
- if (shouldHandleViewLockManually)
- [view unlockFocus];
+ if (shouldHandleViewLockManually)
+ [view unlockFocus];
- if (drawingOutsideOfDisplayCycle) {
- redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]);
- [view.window flushWindow];
- }
+ if (drawingOutsideOfDisplayCycle) {
+ redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]);
+ [view.window flushWindow];
}
- // Done flushing to either CALayer or NSWindow backingstore
+
+ // Done flushing to NSWindow backingstore
QCocoaWindow *topLevelCocoaWindow = static_cast<QCocoaWindow *>(topLevelWindow->handle());
if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) {
@@ -251,7 +237,7 @@ void QCocoaBackingStore::flush(QWindow *window, const QRegion &region, const QPo
https://trac.webkit.org/changeset/85376/webkit
*/
-void QCocoaBackingStore::redrawRoundedBottomCorners(CGRect windowRect) const
+void QNSWindowBackingStore::redrawRoundedBottomCorners(CGRect windowRect) const
{
#if !defined(QT_APPLE_NO_PRIVATE_APIS)
Q_ASSERT(this->window()->handle());
@@ -285,4 +271,345 @@ void QCocoaBackingStore::redrawRoundedBottomCorners(CGRect windowRect) const
#endif
}
+// ----------------------------------------------------------------------------
+
+// https://stackoverflow.com/a/52722575/2761869
+template<class R>
+struct backwards_t {
+ R r;
+ constexpr auto begin() const { using std::rbegin; return rbegin(r); }
+ constexpr auto begin() { using std::rbegin; return rbegin(r); }
+ constexpr auto end() const { using std::rend; return rend(r); }
+ constexpr auto end() { using std::rend; return rend(r); }
+};
+template<class R>
+constexpr backwards_t<R> backwards(R&& r) { return {std::forward<R>(r)}; }
+
+QCALayerBackingStore::QCALayerBackingStore(QWindow *window)
+ : QPlatformBackingStore(window)
+{
+ qCDebug(lcQpaBackingStore) << "Creating QCALayerBackingStore for" << window;
+ m_buffers.resize(1);
+}
+
+QCALayerBackingStore::~QCALayerBackingStore()
+{
+}
+
+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";
+
+ m_requestedSize = size;
+}
+
+void QCALayerBackingStore::beginPaint(const QRegion &region)
+{
+ Q_UNUSED(region);
+
+ QMacAutoReleasePool pool;
+
+ qCInfo(lcQpaBackingStore) << "Beginning paint of" << region << "into backingstore of" << m_requestedSize;
+
+ ensureBackBuffer(); // Find an unused back buffer, or reserve space for a new one
+
+ const bool bufferWasRecreated = recreateBackBufferIfNeeded();
+
+ m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess);
+
+ // Although undocumented, QBackingStore::beginPaint expects the painted region
+ // to be cleared before use if the window has a surface format with an alpha.
+ // Fresh IOSurfaces are already cleared, so we don't need to clear those.
+ if (!bufferWasRecreated && window()->format().hasAlpha()) {
+ qCDebug(lcQpaBackingStore) << "Clearing" << region << "before use";
+ QPainter painter(m_buffers.back()->asImage());
+ painter.setCompositionMode(QPainter::CompositionMode_Source);
+ for (const QRect &rect : region)
+ painter.fillRect(rect, Qt::transparent);
+ }
+
+ m_paintedRegion += region;
+}
+
+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 │
+ // └───────┴───────┴───────┴─────┴──────┘
+ for (const auto &buffer : m_buffers) {
+ qCDebug(lcQpaBackingStore).nospace() << " "
+ << (buffer == m_buffers.front() ? "front" :
+ buffer == m_buffers.back() ? " back" :
+ "spare"
+ ) << ": " << buffer.get();
+ }
+ }
+
+ // Ensure our back buffer is ready to draw into. If not, find a buffer that
+ // is not in use, or reserve space for a new buffer if none can be found.
+ for (auto &buffer : backwards(m_buffers)) {
+ if (!buffer || !buffer->isInUse()) {
+ // Buffer is okey to use, swap if necessary
+ if (buffer != m_buffers.back())
+ std::swap(buffer, m_buffers.back());
+ qCDebug(lcQpaBackingStore) << "Using back buffer" << m_buffers.back().get();
+
+ static const int kMaxSwapChainDepth = 3;
+ if (m_buffers.size() > kMaxSwapChainDepth) {
+ qCDebug(lcQpaBackingStore) << "Reducing swap chain depth to" << kMaxSwapChainDepth;
+ m_buffers.erase(std::next(m_buffers.begin(), 1), std::prev(m_buffers.end(), 2));
+ }
+
+ break;
+ } else if (buffer == m_buffers.front()) {
+ // We've exhausted the available buffers, make room for a new one
+ const int swapChainDepth = m_buffers.size() + 1;
+ qCDebug(lcQpaBackingStore) << "Available buffers exhausted, increasing swap chain depth to" << swapChainDepth;
+ m_buffers.resize(swapChainDepth);
+ break;
+ }
+ }
+
+ Q_ASSERT(!m_buffers.back() || !m_buffers.back()->isInUse());
+}
+
+// Disabled until performance issue on 5K iMac Pro has been investigated further,
+// as rounding up during resize will typically result in full screen buffer sizes
+// and low frame rate also for smaller window sizes.
+#define USE_LAZY_BUFFER_ALLOCATION_DURING_LIVE_WINDOW_RESIZE 0
+
+bool QCALayerBackingStore::recreateBackBufferIfNeeded()
+{
+ const qreal devicePixelRatio = window()->devicePixelRatio();
+ QSize requestedBufferSize = m_requestedSize * devicePixelRatio;
+
+ const NSView *backingStoreView = static_cast<QCocoaWindow *>(window()->handle())->view();
+ Q_UNUSED(backingStoreView);
+
+ auto bufferSizeMismatch = [&](const QSize requested, const QSize actual) {
+#if USE_LAZY_BUFFER_ALLOCATION_DURING_LIVE_WINDOW_RESIZE
+ if (backingStoreView.inLiveResize) {
+ // Prevent over-eager buffer allocation during window resize by reusing larger buffers
+ return requested.width() > actual.width() || requested.height() > actual.height();
+ }
+#endif
+ return requested != actual;
+ };
+
+ if (!m_buffers.back() || bufferSizeMismatch(requestedBufferSize, m_buffers.back()->size())) {
+#if USE_LAZY_BUFFER_ALLOCATION_DURING_LIVE_WINDOW_RESIZE
+ if (backingStoreView.inLiveResize) {
+ // Prevent over-eager buffer allocation during window resize by rounding up
+ QSize nativeScreenSize = window()->screen()->geometry().size() * devicePixelRatio;
+ requestedBufferSize = QSize(qNextPowerOfTwo(requestedBufferSize.width()),
+ qNextPowerOfTwo(requestedBufferSize.height())).boundedTo(nativeScreenSize);
+ }
+#endif
+
+ qCInfo(lcQpaBackingStore) << "Creating surface of" << requestedBufferSize
+ << "based on requested" << m_requestedSize << "and dpr =" << devicePixelRatio;
+
+ static auto pixelFormat = QImage::toPixelFormat(QImage::Format_ARGB32_Premultiplied);
+
+ NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view();
+ auto colorSpace = QCFType<CGColorSpaceRef>::constructFromGet(view.window.screen.colorSpace.CGColorSpace);
+
+ m_buffers.back().reset(new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace));
+ return true;
+ }
+
+ return false;
+}
+
+QPaintDevice *QCALayerBackingStore::paintDevice()
+{
+ Q_ASSERT(m_buffers.back());
+ return m_buffers.back()->asImage();
+}
+
+void QCALayerBackingStore::endPaint()
+{
+ qCInfo(lcQpaBackingStore) << "Paint ended with painted region" << m_paintedRegion;
+ m_buffers.back()->unlock();
+}
+
+void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion &region, const QPoint &offset)
+{
+ Q_UNUSED(region);
+ Q_UNUSED(offset);
+
+ if (!prepareForFlush())
+ return;
+
+ QMacAutoReleasePool pool;
+
+ NSView *backingStoreView = static_cast<QCocoaWindow *>(window()->handle())->view();
+ NSView *flushedView = static_cast<QCocoaWindow *>(flushedWindow->handle())->view();
+
+ id backBufferSurface = (__bridge id)m_buffers.back()->surface();
+ if (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.
+ // The layer already knows about the back buffer, and we don't need to re-apply
+ // it to pick up the 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
+ // means CA won't try to throttle us if we flush too fast, and we'll coalesce our flush
+ // with other pending view and layer updates.
+ backingStoreView.window.viewsNeedDisplay = YES;
+
+ if (window()->format().swapBehavior() == QSurfaceFormat::SingleBuffer) {
+ // The private API [CALayer reloadValueForKeyPath:@"contents"] would be preferable,
+ // but barring any side effects or performance issues we opt for the hammer for now.
+ flushedView.layer.contents = nil;
+ }
+
+ qCInfo(lcQpaBackingStore) << "Flushing" << backBufferSurface
+ << "to" << flushedView.layer << "of" << flushedView;
+
+ flushedView.layer.contents = backBufferSurface;
+
+ if (flushedView != backingStoreView) {
+ const CGSize backingStoreSize = backingStoreView.bounds.size;
+ flushedView.layer.contentsRect = CGRectApplyAffineTransform(
+ [flushedView convertRect:flushedView.bounds toView:backingStoreView],
+ // The contentsRect is in unit coordinate system
+ CGAffineTransformMakeScale(1.0 / backingStoreSize.width, 1.0 / backingStoreSize.height));
+ }
+
+ // 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.
+
+ // 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.
+}
+
+void QCALayerBackingStore::composeAndFlush(QWindow *window, const QRegion &region, const QPoint &offset,
+ QPlatformTextureList *textures, bool translucentBackground)
+{
+ if (!prepareForFlush())
+ return;
+
+ QPlatformBackingStore::composeAndFlush(window, region, offset, textures, translucentBackground);
+}
+
+QPlatformGraphicsBuffer *QCALayerBackingStore::graphicsBuffer() const
+{
+ return m_buffers.back().get();
+}
+
+bool QCALayerBackingStore::prepareForFlush()
+{
+ 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;
+ else
+ buffer->dirtyRegion += m_paintedRegion;
+ }
+
+ // 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();
+
+ const QRect frontSurfaceBounds(QPoint(0, 0), m_buffers.front()->size());
+ const qreal sourceDevicePixelRatio = frontBuffer->devicePixelRatio();
+
+ m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess);
+ QPainter painter(m_buffers.back()->asImage());
+ painter.setCompositionMode(QPainter::CompositionMode_Source);
+
+ // 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);
+
+ for (const QRect &rect : preserveRegion) {
+ QRect sourceRect(rect.topLeft() * sourceDevicePixelRatio, rect.size() * sourceDevicePixelRatio);
+ QRect targetRect(rect.topLeft() * targetDevicePixelRatio, rect.size() * targetDevicePixelRatio);
+
+#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);
+ }
+
+ m_buffers.back()->unlock();
+ m_buffers.front()->unlock();
+
+ // The back buffer is now completely in sync, ready to be presented
+ m_buffers.back()->dirtyRegion = QRegion();
+ }
+
+ // Prepare for another round of painting
+ m_paintedRegion = QRegion();
+
+ return true;
+}
+
+// ----------------------------------------------------------------------------
+
+QCALayerBackingStore::GraphicsBuffer::GraphicsBuffer(const QSize &size, qreal devicePixelRatio,
+ const QPixelFormat &format, QCFType<CGColorSpaceRef> colorSpace)
+ : QIOSurfaceGraphicsBuffer(size, format, colorSpace)
+ , dirtyRegion(0, 0, size.width() / devicePixelRatio, size.height() / devicePixelRatio)
+ , m_devicePixelRatio(devicePixelRatio)
+{
+}
+
+QImage *QCALayerBackingStore::GraphicsBuffer::asImage()
+{
+ if (m_image.isNull()) {
+ qCDebug(lcQpaBackingStore) << "Setting up paint device for" << this;
+ CFRetain(surface());
+ m_image = QImage(data(), size().width(), size().height(),
+ bytesPerLine(), QImage::toImageFormat(format()),
+ QImageCleanupFunction(CFRelease), surface());
+ m_image.setDevicePixelRatio(m_devicePixelRatio);
+ }
+
+ Q_ASSERT_X(m_image.constBits() == data(), "QCALayerBackingStore",
+ "IOSurfaces should have have a fixed location in memory once created");
+
+ return &m_image;
+}
+
QT_END_NAMESPACE
diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.mm b/src/plugins/platforms/cocoa/qcocoaintegration.mm
index affbee35b7..6d6c66e242 100644
--- a/src/plugins/platforms/cocoa/qcocoaintegration.mm
+++ b/src/plugins/platforms/cocoa/qcocoaintegration.mm
@@ -407,7 +407,16 @@ QPlatformOpenGLContext *QCocoaIntegration::createPlatformOpenGLContext(QOpenGLCo
QPlatformBackingStore *QCocoaIntegration::createPlatformBackingStore(QWindow *window) const
{
- return new QCocoaBackingStore(window);
+ QCocoaWindow *platformWindow = static_cast<QCocoaWindow*>(window->handle());
+ if (!platformWindow) {
+ qWarning() << window << "must be created before being used with a backingstore";
+ return nullptr;
+ }
+
+ if (platformWindow->view().layer)
+ return new QCALayerBackingStore(window);
+ else
+ return new QNSWindowBackingStore(window);
}
QAbstractEventDispatcher *QCocoaIntegration::createEventDispatcher() const
diff --git a/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h b/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h
new file mode 100644
index 0000000000..872773cb7a
--- /dev/null
+++ b/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h
@@ -0,0 +1,77 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 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$
+**
+****************************************************************************/
+
+#ifndef QIOSURFACEGRAPHICSBUFFER_H
+#define QIOSURFACEGRAPHICSBUFFER_H
+
+#include <qpa/qplatformgraphicsbuffer.h>
+#include <private/qcore_mac_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QIOSurfaceGraphicsBuffer : public QPlatformGraphicsBuffer
+{
+public:
+ QIOSurfaceGraphicsBuffer(const QSize &size, const QPixelFormat &format, QCFType<CGColorSpaceRef> colorSpace);
+ ~QIOSurfaceGraphicsBuffer();
+
+ const uchar *data() const override;
+ uchar *data() override;
+ int bytesPerLine() const override;
+
+ IOSurfaceRef surface();
+ bool isInUse() const;
+
+protected:
+ bool doLock(AccessTypes access, const QRect &rect) override;
+ void doUnlock() override;
+
+private:
+ QCFType<IOSurfaceRef> m_surface;
+
+ friend QDebug operator<<(QDebug, const QIOSurfaceGraphicsBuffer *);
+};
+
+#ifndef QT_NO_DEBUG_STREAM
+QDebug operator<<(QDebug, const QIOSurfaceGraphicsBuffer *);
+#endif
+
+QT_END_NAMESPACE
+
+#endif // QIOSURFACEGRAPHICSBUFFER_H
diff --git a/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.mm b/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.mm
new file mode 100644
index 0000000000..a367487e85
--- /dev/null
+++ b/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.mm
@@ -0,0 +1,188 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 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$
+**
+****************************************************************************/
+
+#include "qiosurfacegraphicsbuffer.h"
+
+#include <QtCore/qdebug.h>
+#include <QtCore/qloggingcategory.h>
+
+#include <CoreGraphics/CoreGraphics.h>
+#include <IOSurface/IOSurface.h>
+
+// CGColorSpaceCopyPropertyList is available on 10.12 and above,
+// but was only added in the 10.14 SDK, so declare it just in case.
+extern "C" CFPropertyListRef CGColorSpaceCopyPropertyList(CGColorSpaceRef space);
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(lcQpaIOSurface, "qt.qpa.backingstore.iosurface");
+
+QIOSurfaceGraphicsBuffer::QIOSurfaceGraphicsBuffer(const QSize &size, const QPixelFormat &format, QCFType<CGColorSpaceRef> colorSpace)
+ : QPlatformGraphicsBuffer(size, format)
+{
+ const size_t width = size.width();
+ const size_t height = size.height();
+
+ Q_ASSERT(width <= IOSurfaceGetPropertyMaximum(kIOSurfaceWidth));
+ Q_ASSERT(height <= IOSurfaceGetPropertyMaximum(kIOSurfaceHeight));
+
+ static const char bytesPerElement = 4;
+
+ const size_t bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, width * bytesPerElement);
+ const size_t totalBytes = IOSurfaceAlignProperty(kIOSurfaceAllocSize, height * bytesPerRow);
+
+ NSDictionary *options = @{
+ (id)kIOSurfaceWidth: @(width),
+ (id)kIOSurfaceHeight: @(height),
+ (id)kIOSurfacePixelFormat: @(unsigned('BGRA')),
+ (id)kIOSurfaceBytesPerElement: @(bytesPerElement),
+ (id)kIOSurfaceBytesPerRow: @(bytesPerRow),
+ (id)kIOSurfaceAllocSize: @(totalBytes),
+ };
+
+ m_surface = IOSurfaceCreate((CFDictionaryRef)options);
+ Q_ASSERT(m_surface);
+
+ Q_ASSERT(size_t(bytesPerLine()) == bytesPerRow);
+ Q_ASSERT(size_t(byteCount()) == totalBytes);
+
+ if (colorSpace) {
+ IOSurfaceSetValue(m_surface, CFSTR("IOSurfaceColorSpace"),
+ QCFType<CFPropertyListRef>(CGColorSpaceCopyPropertyList(colorSpace)));
+ }
+}
+
+QIOSurfaceGraphicsBuffer::~QIOSurfaceGraphicsBuffer()
+{
+}
+
+const uchar *QIOSurfaceGraphicsBuffer::data() const
+{
+ return (const uchar *)IOSurfaceGetBaseAddress(m_surface);
+}
+
+uchar *QIOSurfaceGraphicsBuffer::data()
+{
+ return (uchar *)IOSurfaceGetBaseAddress(m_surface);
+}
+
+int QIOSurfaceGraphicsBuffer::bytesPerLine() const
+{
+ return IOSurfaceGetBytesPerRow(m_surface);
+}
+
+IOSurfaceRef QIOSurfaceGraphicsBuffer::surface()
+{
+ return m_surface;
+}
+
+bool QIOSurfaceGraphicsBuffer::isInUse() const
+{
+ return IOSurfaceIsInUse(m_surface);
+}
+
+IOSurfaceLockOptions lockOptionsForAccess(QPlatformGraphicsBuffer::AccessTypes access)
+{
+ IOSurfaceLockOptions lockOptions = 0;
+ if (!(access & QPlatformGraphicsBuffer::SWWriteAccess))
+ lockOptions |= kIOSurfaceLockReadOnly;
+ return lockOptions;
+}
+
+bool QIOSurfaceGraphicsBuffer::doLock(AccessTypes access, const QRect &rect)
+{
+ Q_UNUSED(rect);
+ Q_ASSERT(!isLocked());
+
+ qCDebug(lcQpaIOSurface) << "Locking" << this << "for" << access;
+
+ // FIXME: Teach QPlatformBackingStore::composeAndFlush about non-2D texture
+ // targets, so that we can use CGLTexImageIOSurface2D to support TextureAccess.
+ if (access & (TextureAccess | HWCompositor))
+ return false;
+
+ auto lockOptions = lockOptionsForAccess(access);
+
+ // Try without read-back first
+ lockOptions |= kIOSurfaceLockAvoidSync;
+ kern_return_t ret = IOSurfaceLock(m_surface, lockOptions, nullptr);
+ if (ret == kIOSurfaceSuccess)
+ return true;
+
+ if (ret == kIOReturnCannotLock) {
+ qCWarning(lcQpaIOSurface) << "Locking of" << this << "requires read-back";
+ lockOptions ^= kIOSurfaceLockAvoidSync;
+ ret = IOSurfaceLock(m_surface, lockOptions, nullptr);
+ }
+
+ if (ret != kIOSurfaceSuccess) {
+ qCWarning(lcQpaIOSurface) << "Failed to lock" << this << ret;
+ return false;
+ }
+
+ return true;
+}
+
+void QIOSurfaceGraphicsBuffer::doUnlock()
+{
+ qCDebug(lcQpaIOSurface) << "Unlocking" << this << "from" << isLocked();
+
+ auto lockOptions = lockOptionsForAccess(isLocked());
+ bool success = IOSurfaceUnlock(m_surface, lockOptions, nullptr) == kIOSurfaceSuccess;
+ Q_ASSERT_X(success, "QIOSurfaceGraphicsBuffer", "Unlocking surface should succeed");
+}
+
+#ifndef QT_NO_DEBUG_STREAM
+QDebug operator<<(QDebug debug, const QIOSurfaceGraphicsBuffer *graphicsBuffer)
+{
+ QDebugStateSaver saver(debug);
+ debug.nospace();
+ debug << "QIOSurfaceGraphicsBuffer(" << (const void *)graphicsBuffer;
+ if (graphicsBuffer) {
+ debug << ", surface=" << graphicsBuffer->m_surface;
+ debug << ", size=" << graphicsBuffer->size();
+ debug << ", isLocked=" << bool(graphicsBuffer->isLocked());
+ debug << ", isInUse=" << graphicsBuffer->isInUse();
+ }
+ debug << ')';
+ return debug;
+}
+#endif // !QT_NO_DEBUG_STREAM
+
+QT_END_NAMESPACE
diff --git a/src/plugins/platforms/cocoa/qnsview_drawing.mm b/src/plugins/platforms/cocoa/qnsview_drawing.mm
index e9af90a45c..0cebc8d98d 100644
--- a/src/plugins/platforms/cocoa/qnsview_drawing.mm
+++ b/src/plugins/platforms/cocoa/qnsview_drawing.mm
@@ -163,6 +163,13 @@
return NSViewLayerContentsRedrawDuringViewResize;
}
+- (NSViewLayerContentsPlacement)layerContentsPlacement
+{
+ // Always place the layer at top left without any automatic scaling,
+ // so that we can re-use larger layers when resizing a window down.
+ return NSViewLayerContentsPlacementTopLeft;
+}
+
- (void)updateMetalLayerDrawableSize:(CAMetalLayer *)layer
{
CGSize drawableSize = layer.bounds.size;