macOS: Modernize QCocoaBackingStore::flush()

Instead of forwarding the flush to the view, using CoreGraphics to blit
the backing store to the window, we do everything in flush(), and use
higher level AppKit APIs to do the blit.

This simplifies the flow and code quite a bit, and also supports blitting
of individual regions in a flush instead of the whole bounding rect.

Change-Id: I2173c1a7763fe652a94125c7e3ae93a655412cd3
Reviewed-by: Jake Petroules <jake.petroules@qt.io>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Tor Arne Vestbø 2017-07-11 18:08:51 +02:00
parent 047e0e5118
commit f25b4d2fe3
5 changed files with 147 additions and 145 deletions

View File

@ -53,6 +53,7 @@ public:
void flush(QWindow *, const QRegion &, const QPoint &) Q_DECL_OVERRIDE;
private:
bool windowHasUnifiedToolbar() const;
QImage::Format format() const Q_DECL_OVERRIDE;
};

View File

@ -44,6 +44,8 @@
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcCocoaBackingStore, "qt.qpa.cocoa.backingstore");
QCocoaBackingStore::QCocoaBackingStore(QWindow *window)
: QRasterBackingStore(window)
{
@ -51,26 +53,157 @@ QCocoaBackingStore::QCocoaBackingStore(QWindow *window)
QCocoaBackingStore::~QCocoaBackingStore()
{
if (QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window()->handle()))
[qnsview_cast(cocoaWindow->view()) clearBackingStore:this];
}
bool QCocoaBackingStore::windowHasUnifiedToolbar() const
{
Q_ASSERT(window()->handle());
return static_cast<QCocoaWindow *>(window()->handle())->m_drawContentBorderGradient;
}
QImage::Format QCocoaBackingStore::format() const
{
QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window()->handle());
if (cocoaWindow && cocoaWindow->m_drawContentBorderGradient)
if (windowHasUnifiedToolbar())
return QImage::Format_ARGB32_Premultiplied;
return QRasterBackingStore::format();
}
#if !QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_12)
static const NSCompositingOperation NSCompositingOperationCopy = NSCompositeCopy;
static const NSCompositingOperation NSCompositingOperationSourceOver = NSCompositeSourceOver;
#endif
/*!
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 QCocoaBackingStore::flush(QWindow *window, const QRegion &region, const QPoint &offset)
{
if (m_image.isNull())
return;
if (QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()))
[qnsview_cast(cocoaWindow->view()) flushBackingStore:this region:region offset:offset];
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 (lcCocoaBackingStore().isDebugEnabled()) {
QString targetViewDescription;
if (view != topLevelView) {
QDebug targetDebug(&targetViewDescription);
targetDebug << "onto" << topLevelView << "at" << offset;
}
qCDebug(lcCocoaBackingStore) << "Flushing" << region << "of" << view << 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 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();
// If the flushed window is a content view, and not in unified toolbar mode,
// we can get away with copying the backingstore instead of blending.
const NSCompositingOperation compositingOperation = static_cast<QCocoaWindow *>(
window->handle())->isContentView() && !windowHasUnifiedToolbar() ?
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");
// Create temporary image to use for blitting, without copying image data
NSImage *backingStoreImage = [[[NSImage alloc]
initWithCGImage:QCFType<CGImageRef>(m_image.toCGImage()) size:NSZeroSize] autorelease];
if ([topLevelView hasMask]) {
// FIXME: Implement via NSBezierPath and addClip
CGRect boundingRect = region.boundingRect().toCGRect();
QCFType<CGImageRef> subMask = CGImageCreateWithImageInRect([topLevelView maskImage], boundingRect);
CGContextClipToMask(graphicsContext.CGContext, boundingRect, subMask);
}
for (const QRect &viewLocalRect : region) {
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();
if (windowHasUnifiedToolbar())
NSDrawWindowBackground(viewRect);
[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)
[view unlockFocus];
if (drawingOutsideOfDisplayCycle)
[view.window flushWindow];
// FIXME: Tie to changing window flags and/or mask instead
[view invalidateWindowShadowIfNeeded];
}
QT_END_NAMESPACE

View File

@ -1059,8 +1059,6 @@ void QCocoaWindow::handleGeometryChange()
// calls, which Qt and Qt applications do not expect.
if (!m_inSetGeometry)
QWindowSystemInterface::flushWindowSystemEvents();
else if (newGeometry.size() != geometry().size())
[qnsview_cast(m_view) clearBackingStore];
}
void QCocoaWindow::handleExposeEvent(const QRegion &region)

View File

@ -51,15 +51,12 @@
QT_BEGIN_NAMESPACE
class QCocoaWindow;
class QCocoaBackingStore;
class QCocoaGLContext;
QT_END_NAMESPACE
Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper));
@interface QT_MANGLE_NAMESPACE(QNSView) : NSView <NSTextInputClient> {
QCocoaBackingStore* m_backingStore;
QPoint m_backingStoreOffset;
QRegion m_maskRegion;
CGImageRef m_maskImage;
bool m_shouldInvalidateWindowShadow;
@ -93,13 +90,10 @@ Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper));
#ifndef QT_NO_OPENGL
- (void)setQCocoaGLContext:(QCocoaGLContext *)context;
#endif
- (void)flushBackingStore:(QCocoaBackingStore *)backingStore region:(const QRegion &)region offset:(QPoint)offset;
- (void)clearBackingStore;
- (void)clearBackingStore:(QCocoaBackingStore *)backingStore;
- (void)setMaskRegion:(const QRegion *)region;
- (CGImageRef)maskImage;
- (void)invalidateWindowShadowIfNeeded;
- (void)drawRect:(NSRect)dirtyRect;
- (void)drawBackingStoreUsingCoreGraphics:(NSRect)dirtyRect;
- (void)textInputContextKeyboardSelectionDidChangeNotification : (NSNotification *) textInputContextKeyboardSelectionDidChangeNotification;
- (void)viewDidHide;
- (void)removeFromSuperview;

View File

@ -134,7 +134,6 @@ static QTouchDevice *touchDevice = 0;
- (id) init
{
if (self = [super initWithFrame:NSZeroRect]) {
m_backingStore = 0;
m_maskImage = 0;
m_shouldInvalidateWindowShadow = false;
m_buttons = Qt::NoButton;
@ -267,11 +266,6 @@ static QTouchDevice *touchDevice = 0;
}
}
- (void)viewDidMoveToWindow
{
m_backingStore = Q_NULLPTR;
}
- (QWindow *)topLevelWindow
{
if (!m_platformWindow)
@ -316,67 +310,6 @@ static QTouchDevice *touchDevice = 0;
[super removeFromSuperview];
}
- (void)flushBackingStore:(QCocoaBackingStore *)backingStore region:(const QRegion &)region offset:(QPoint)offset
{
qCDebug(lcQpaCocoaWindow) << "[QNSView flushBackingStore:]" << m_platformWindow->window() << region.rectCount() << region.boundingRect() << offset;
m_backingStore = backingStore;
m_backingStoreOffset = offset * m_backingStore->paintDevice()->devicePixelRatio();
// FIXME: Clean up this method now that the drawRect logic has been merged into it
const NSRect dirtyRect = region.boundingRect().toCGRect();
// 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] != self;
if (shouldHandleViewLockManually && ![self lockFocusIfCanDraw]) {
qWarning() << "failed to lock focus of" << self;
return;
}
if (m_platformWindow->m_drawContentBorderGradient)
NSDrawWindowBackground(dirtyRect);
[self drawBackingStoreUsingCoreGraphics:dirtyRect];
if (shouldHandleViewLockManually)
[self unlockFocus];
if (drawingOutsideOfDisplayCycle)
[self.window flushWindow];
[self invalidateWindowShadowIfNeeded];
}
- (void)clearBackingStore
{
m_backingStore = nullptr;
}
- (void)clearBackingStore:(QCocoaBackingStore *)backingStore
{
if (backingStore == m_backingStore)
m_backingStore = 0;
}
- (BOOL) hasMask
{
return !m_maskRegion.isEmpty();
@ -418,6 +351,11 @@ static QTouchDevice *touchDevice = 0;
m_maskImage = qt_mac_toCGImageMask(maskImage);
}
- (CGImageRef)maskImage
{
return m_maskImage;
}
- (void)invalidateWindowShadowIfNeeded
{
if (m_shouldInvalidateWindowShadow && m_platformWindow->isContentView()) {
@ -452,68 +390,6 @@ static QTouchDevice *touchDevice = 0;
m_platformWindow->handleExposeEvent(exposedRegion);
}
// Draws the backing store content to the QNSView using Core Graphics.
// This function assumes that the QNSView is in a configuration that
// supports Core Graphics, such as "classic" mode or layer mode with
// the default layer.
- (void)drawBackingStoreUsingCoreGraphics:(NSRect)dirtyRect
{
if (!m_backingStore)
return;
// Calculate source and target rects. The target rect is the dirtyRect:
CGRect dirtyWindowRect = NSRectToCGRect(dirtyRect);
// The backing store source rect will be larger on retina displays.
// Scale dirtyRect by the device pixel ratio:
const qreal devicePixelRatio = m_backingStore->paintDevice()->devicePixelRatio();
CGRect dirtyBackingRect = CGRectMake(dirtyRect.origin.x * devicePixelRatio,
dirtyRect.origin.y * devicePixelRatio,
dirtyRect.size.width * devicePixelRatio,
dirtyRect.size.height * devicePixelRatio);
NSGraphicsContext *nsGraphicsContext = [NSGraphicsContext currentContext];
CGContextRef cgContext = (CGContextRef) [nsGraphicsContext graphicsPort];
// Translate coordiate system from CoreGraphics (bottom-left) to NSView (top-left):
CGContextSaveGState(cgContext);
int dy = dirtyWindowRect.origin.y + CGRectGetMaxY(dirtyWindowRect);
CGContextTranslateCTM(cgContext, 0, dy);
CGContextScaleCTM(cgContext, 1, -1);
// If a mask is set, modify the sub image accordingly:
CGImageRef subMask = 0;
if (m_maskImage) {
subMask = CGImageCreateWithImageInRect(m_maskImage, dirtyWindowRect);
CGContextClipToMask(cgContext, dirtyWindowRect, subMask);
}
// Clip out and draw the correct sub image from the (shared) backingstore:
CGRect backingStoreRect = CGRectMake(
dirtyBackingRect.origin.x + m_backingStoreOffset.x(),
dirtyBackingRect.origin.y + m_backingStoreOffset.y(),
dirtyBackingRect.size.width,
dirtyBackingRect.size.height
);
CGImageRef bsCGImage = qt_mac_toCGImage(m_backingStore->toImage());
CGImageRef cleanImg = CGImageCreateWithImageInRect(bsCGImage, backingStoreRect);
// Optimization: Copy frame buffer content instead of blending for
// top-level windows where Qt fills the entire window content area.
// (But don't overpaint the title-bar gradient)
if (m_platformWindow->isContentView() && !m_platformWindow->m_drawContentBorderGradient)
CGContextSetBlendMode(cgContext, kCGBlendModeCopy);
CGContextDrawImage(cgContext, dirtyWindowRect, cleanImg);
// Clean-up:
CGContextRestoreGState(cgContext);
CGImageRelease(cleanImg);
CGImageRelease(subMask);
CGImageRelease(bsCGImage);
}
- (BOOL)isFlipped
{
return YES;