macOS: Flush sublayers via separate IOSurface backingstores

Flushing sublayers via QImage copies of the root IOSurface was causing
performance regressions due to the constant allocations of new images
each frame.

We now re-use the QCALayerBackingStore implementation for sublayers,
which gives a dynamic swap-chain.

We're still paying the CPU cost of the copy from the root backingstore
to the layered backingstores, as well as the memory cost, but at least
improves the situation.

We do not try to be smart and paint directly into the sublayers,
as that would leave the root backingstore stale, potentially causing
glitches when views are repositioned. Investigating this is left
for future work.

Fixes: QTBUG-82986
Change-Id: I758a3d8e1e40e2ed4fe6bc590a4a5a988d87a3a7
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
(cherry picked from commit ce2d68ebe1aefeae78ff2fd8ec5ff7e20790ef69)
This commit is contained in:
Tor Arne Vestbø 2020-03-24 10:08:49 +01:00
parent f10b31fc1b
commit 02fa39ed22
2 changed files with 68 additions and 25 deletions

View File

@ -47,6 +47,8 @@
#include <QScopedPointer>
#include "qiosurfacegraphicsbuffer.h"
#include <unordered_map>
QT_BEGIN_NAMESPACE
class QCocoaBackingStore : public QRasterBackingStore
@ -71,8 +73,9 @@ private:
void redrawRoundedBottomCorners(CGRect) const;
};
class QCALayerBackingStore : public QCocoaBackingStore
class QCALayerBackingStore : public QObject, public QCocoaBackingStore
{
Q_OBJECT
public:
QCALayerBackingStore(QWindow *window);
~QCALayerBackingStore();
@ -119,6 +122,11 @@ private:
QMacNotificationObserver m_backingPropertiesObserver;
std::list<std::unique_ptr<GraphicsBuffer>> m_buffers;
void flushSubWindow(QWindow *window);
std::unordered_map<QWindow*, std::unique_ptr<QCALayerBackingStore>> m_subWindowBackingstores;
void windowDestroyed(QObject *object);
bool m_clearSurfaceOnPaint = true;
};
QT_END_NAMESPACE

View File

@ -382,7 +382,7 @@ void QCALayerBackingStore::beginPaint(const QRegion &region)
// 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()) {
if (m_clearSurfaceOnPaint && !bufferWasRecreated && window()->format().hasAlpha()) {
qCDebug(lcQpaBackingStore) << "Clearing" << region << "before use";
QPainter painter(m_buffers.back()->asImage());
painter.setCompositionMode(QPainter::CompositionMode_Source);
@ -511,9 +511,13 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion &region,
if (!prepareForFlush())
return;
if (flushedWindow != window()) {
flushSubWindow(flushedWindow);
return;
}
QMacAutoReleasePool pool;
NSView *backingStoreView = static_cast<QCocoaWindow *>(window()->handle())->view();
NSView *flushedView = static_cast<QCocoaWindow *>(flushedWindow->handle())->view();
// If the backingstore is just flushed, without being painted to first, then we may
@ -548,7 +552,7 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion &region,
// 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;
flushedView.window.viewsNeedDisplay = YES;
if (window()->format().swapBehavior() == QSurfaceFormat::SingleBuffer) {
// The private API [CALayer reloadValueForKeyPath:@"contents"] would be preferable,
@ -556,28 +560,10 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion &region,
flushedView.layer.contents = nil;
}
if (flushedView == backingStoreView) {
qCInfo(lcQpaBackingStore) << "Flushing" << backBufferSurface
<< "to" << flushedView.layer << "of" << flushedView;
flushedView.layer.contents = backBufferSurface;
} else {
auto subviewRect = [flushedView convertRect:flushedView.bounds toView:backingStoreView];
auto scale = flushedView.layer.contentsScale;
subviewRect = CGRectApplyAffineTransform(subviewRect, CGAffineTransformMakeScale(scale, scale));
qCInfo(lcQpaBackingStore) << "Flushing" << backBufferSurface
<< "to" << flushedView.layer << "of" << flushedView;
// We make a copy of the image data up front, which means we don't
// need to mark the IOSurface as being in use. FIXME: Investigate
// if there's a cheaper way to get sub-image data to a layer.
m_buffers.back()->lock(QPlatformGraphicsBuffer::SWReadAccess);
QImage subImage = m_buffers.back()->asImage()->copy(QRectF::fromCGRect(subviewRect).toRect());
m_buffers.back()->unlock();
qCInfo(lcQpaBackingStore) << "Flushing" << subImage
<< "to" << flushedView.layer << "of subview" << flushedView;
QCFType<CGImageRef> cgImage = CGImageCreateCopyWithColorSpace(
QCFType<CGImageRef>(subImage.toCGImage()), colorSpace());
flushedView.layer.contents = (__bridge id)static_cast<CGImageRef>(cgImage);
}
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
@ -589,6 +575,53 @@ void QCALayerBackingStore::flush(QWindow *flushedWindow, const QRegion &region,
// the window server.
}
void QCALayerBackingStore::flushSubWindow(QWindow *subWindow)
{
qCInfo(lcQpaBackingStore) << "Flushing sub-window" << subWindow
<< "via its own backingstore";
auto &subWindowBackingStore = m_subWindowBackingstores[subWindow];
if (!subWindowBackingStore) {
subWindowBackingStore.reset(new QCALayerBackingStore(subWindow));
QObject::connect(subWindow, &QObject::destroyed, this, &QCALayerBackingStore::windowDestroyed);
subWindowBackingStore->m_clearSurfaceOnPaint = false;
}
auto subWindowSize = subWindow->size();
static const auto kNoStaticContents = QRegion();
subWindowBackingStore->resize(subWindowSize, kNoStaticContents);
auto subWindowLocalRect = QRect(QPoint(), subWindowSize);
subWindowBackingStore->beginPaint(subWindowLocalRect);
QPainter painter(subWindowBackingStore->m_buffers.back()->asImage());
painter.setCompositionMode(QPainter::CompositionMode_Source);
NSView *backingStoreView = static_cast<QCocoaWindow *>(window()->handle())->view();
NSView *flushedView = static_cast<QCocoaWindow *>(subWindow->handle())->view();
auto subviewRect = [flushedView convertRect:flushedView.bounds toView:backingStoreView];
auto scale = flushedView.layer.contentsScale;
subviewRect = CGRectApplyAffineTransform(subviewRect, CGAffineTransformMakeScale(scale, scale));
m_buffers.back()->lock(QPlatformGraphicsBuffer::SWReadAccess);
const QImage *backingStoreImage = m_buffers.back()->asImage();
painter.drawImage(subWindowLocalRect, *backingStoreImage, QRectF::fromCGRect(subviewRect));
m_buffers.back()->unlock();
painter.end();
subWindowBackingStore->endPaint();
subWindowBackingStore->flush(subWindow, subWindowLocalRect, QPoint());
qCInfo(lcQpaBackingStore) << "Done flushing sub-window" << subWindow;
}
void QCALayerBackingStore::windowDestroyed(QObject *object)
{
auto *window = static_cast<QWindow*>(object);
qCInfo(lcQpaBackingStore) << "Removing backingstore for sub-window" << window;
m_subWindowBackingstores.erase(window);
}
#ifndef QT_NO_OPENGL
void QCALayerBackingStore::composeAndFlush(QWindow *window, const QRegion &region, const QPoint &offset,
QPlatformTextureList *textures, bool translucentBackground)
@ -722,4 +755,6 @@ QImage *QCALayerBackingStore::GraphicsBuffer::asImage()
return &m_image;
}
#include "moc_qcocoabackingstore.cpp"
QT_END_NAMESPACE