vcl/inc/skia/osx/gdiimpl.hxx | 4 - vcl/osx/salgdiutils.cxx | 98 +++++++++++++++++++++-------- vcl/osx/salmacos.cxx | 7 ++ vcl/skia/osx/gdiimpl.cxx | 142 ++++++++++++++++++++++++++----------------- 4 files changed, 167 insertions(+), 84 deletions(-)
New commits: commit 5b58bc4735e0ec76f9c9dd0ecb047cb662937508 Author: Patrick Luby <guibmac...@gmail.com> AuthorDate: Wed Jun 19 16:03:06 2024 -0400 Commit: Christian Lohmaier <lohmaier+libreoff...@googlemail.com> CommitDate: Thu Jun 27 18:33:57 2024 +0200 tdf#159175 Do not allocate a CGLayer for each NSWindow when using Skia Skia surfaces can be copied directly to an NSWindow's CGContextRef so disable allocation of a CGLayer for each NSWindow to significantly reduce memory usage when Skia is enabled. Change-Id: I8e3001e4f2ae8dd36156c06db68447c6b1bc67df Reviewed-on: https://gerrit.libreoffice.org/c/core/+/169242 Tested-by: Jenkins Reviewed-by: Patrick Luby <guibomac...@gmail.com> (cherry picked from commit 12dbf0e6b6b485a1d73c7e33bd0ecfb13e6efdac) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/169559 Reviewed-by: Christian Lohmaier <lohmaier+libreoff...@googlemail.com> diff --git a/vcl/inc/skia/osx/gdiimpl.hxx b/vcl/inc/skia/osx/gdiimpl.hxx index b97245e86e11..b60280a09e6b 100644 --- a/vcl/inc/skia/osx/gdiimpl.hxx +++ b/vcl/inc/skia/osx/gdiimpl.hxx @@ -44,11 +44,13 @@ public: virtual void Flush(const tools::Rectangle&) override; virtual void WindowBackingPropertiesChanged() override; + CGImageRef createCGImageFromRasterSurface(const NSRect& rDirtyRect, CGPoint& rImageOrigin, + bool& rImageFlipped); + private: virtual int getWindowScaling() const override; virtual void createWindowSurfaceInternal(bool forceRaster = false) override; virtual void flushSurfaceToWindowContext() override; - void flushSurfaceToScreenCG(); static inline sk_sp<SkFontMgr> fontManager; }; diff --git a/vcl/osx/salgdiutils.cxx b/vcl/osx/salgdiutils.cxx index a9445293211c..d7f8ec48eaf0 100644 --- a/vcl/osx/salgdiutils.cxx +++ b/vcl/osx/salgdiutils.cxx @@ -29,6 +29,7 @@ #include <basegfx/range/b2irange.hxx> #include <basegfx/vector/b2ivector.hxx> #include <vcl/svapp.hxx> +#include <vcl/skia/SkiaHelper.hxx> #include <quartz/salgdi.h> #include <quartz/utils.h> @@ -37,7 +38,7 @@ #if HAVE_FEATURE_SKIA #include <tools/sk_app/mac/WindowContextFactory_mac.h> -#include <vcl/skia/SkiaHelper.hxx> +#include <skia/osx/gdiimpl.hxx> #endif static bool bTotalScreenBounds = false; @@ -233,6 +234,10 @@ bool AquaSharedAttributes::checkContext() maLayer.set(nullptr); } + // tdf#159175 no CGLayer is needed for an NSWindow when using Skia + if (SkiaHelper::isVCLSkiaEnabled() && mpFrame->getNSWindow()) + return true; + if (!maContextHolder.isSet()) { const int nBitmapDepth = 32; @@ -297,7 +302,7 @@ bool AquaSharedAttributes::checkContext() * associated window, if any; cf. drawRect event handling * on the frame. */ -void AquaSalGraphics::UpdateWindow( NSRect& ) +void AquaSalGraphics::UpdateWindow( NSRect& rRect ) { if (!maShared.mpFrame) { @@ -305,26 +310,65 @@ void AquaSalGraphics::UpdateWindow( NSRect& ) } NSGraphicsContext* pContext = [NSGraphicsContext currentContext]; - if (maShared.maLayer.isSet() && pContext != nullptr) + if (!pContext) + { + SAL_WARN_IF(!maShared.mpFrame->mbInitShow, "vcl", "UpdateWindow called with no NSGraphicsContext"); + return; + } + + CGImageRef img = nullptr; + CGPoint aImageOrigin = CGPointMake(0, 0); + bool bImageFlipped = false; +#if HAVE_FEATURE_SKIA + if (SkiaHelper::isVCLSkiaEnabled()) + { + // tdf#159175 no CGLayer is needed for an NSWindow when using Skia + // Get a CGImageRef directly from the Skia/Raster surface and draw + // that directly to the NSWindow. + // Note: Skia/Metal will always return a null CGImageRef since it + // draws directly to the NSWindow using the surface's CAMetalLayer. + AquaSkiaSalGraphicsImpl *pBackend = static_cast<AquaSkiaSalGraphicsImpl*>(mpBackend.get()); + if (pBackend) + img = pBackend->createCGImageFromRasterSurface(rRect, aImageOrigin, bImageFlipped); + } + else +#else + (void)rRect; +#endif + if (maShared.maLayer.isSet()) { + maShared.applyXorContext(); + + const CGRect aRectPoints = { CGPointZero, maShared.maLayer.getSizePixels() }; + CGContextSetBlendMode(maShared.maCSContextHolder.get(), kCGBlendModeCopy); + CGContextDrawLayerInRect(maShared.maCSContextHolder.get(), aRectPoints, maShared.maLayer.get()); + + img = CGBitmapContextCreateImage(maShared.maCSContextHolder.get()); + } + + if (img) + { + const float fScale = sal::aqua::getWindowScaling(); CGContextHolder rCGContextHolder([pContext CGContext]); rCGContextHolder.saveState(); - // Related: tdf#155092 translate Y coordinate for height differences - // When in live resize, the NSView's height may have changed before - // the CGLayer has been resized. This causes the CGLayer's content - // to be drawn just above or below the top left corner of the view - // so translate the Y coordinate by any difference between the - // NSView's height and the CGLayer's height. - NSView *pView = maShared.mpFrame->mpNSView; - if (pView) + CGRect aRect = CGRectMake(aImageOrigin.x / fScale, aImageOrigin.y / fScale, CGImageGetWidth(img) / fScale, CGImageGetHeight(img) / fScale); + if (bImageFlipped) { + // Related: tdf#155092 translate Y coordinate of flipped images + // When in live resize, the NSView's height may have changed before + // the surface has been resized. This causes flipped content + // to be drawn just above or below the top left corner of the view + // so translate the Y coordinate using the NSView's height. // Use the NSView's bounds, not its frame, to properly handle // any rotation and/or scaling that might have been already - // applied to the view - CGFloat fTranslateY = [pView bounds].size.height - maShared.maLayer.getSizePoints().height; - CGContextTranslateCTM(rCGContextHolder.get(), 0, fTranslateY); + // applied to the view. + NSView *pView = maShared.mpFrame->mpNSView; + if (pView) + aRect.origin.y = [pView bounds].size.height - aRect.origin.y - aRect.size.height; + else if (maShared.maLayer.isSet()) + aRect.origin.y = maShared.maLayer.getSizePoints().height - aRect.origin.y - aRect.size.height; } CGMutablePathRef rClip = maShared.mpFrame->getClipPath(); @@ -335,23 +379,23 @@ void AquaSalGraphics::UpdateWindow( NSRect& ) CGContextClip(rCGContextHolder.get()); } - maShared.applyXorContext(); - - const CGSize aSize = maShared.maLayer.getSizePoints(); - const CGRect aRect = CGRectMake(0, 0, aSize.width, aSize.height); - const CGRect aRectPoints = { CGPointZero, maShared.maLayer.getSizePixels() }; - CGContextSetBlendMode(maShared.maCSContextHolder.get(), kCGBlendModeCopy); - CGContextDrawLayerInRect(maShared.maCSContextHolder.get(), aRectPoints, maShared.maLayer.get()); - - CGImageRef img = CGBitmapContextCreateImage(maShared.maCSContextHolder.get()); - CGImageRef displayColorSpaceImage = CGImageCreateCopyWithColorSpace(img, [[maShared.mpFrame->getNSWindow() colorSpace] CGColorSpace]); CGContextSetBlendMode(rCGContextHolder.get(), kCGBlendModeCopy); - CGContextDrawImage(rCGContextHolder.get(), aRect, displayColorSpaceImage); - CGImageRelease(img); - CGImageRelease(displayColorSpaceImage); + NSWindow *pWindow = maShared.mpFrame->getNSWindow(); + if (pWindow) + { + CGImageRef displayColorSpaceImage = CGImageCreateCopyWithColorSpace(img, [[maShared.mpFrame->getNSWindow() colorSpace] CGColorSpace]); + CGContextDrawImage(rCGContextHolder.get(), aRect, displayColorSpaceImage); + CGImageRelease(displayColorSpaceImage); + } + else + { + CGContextDrawImage(rCGContextHolder.get(), aRect, img); + } rCGContextHolder.restoreState(); + + CGImageRelease(img); } else { diff --git a/vcl/osx/salmacos.cxx b/vcl/osx/salmacos.cxx index 700b252cf4f3..14e2a80695d9 100644 --- a/vcl/osx/salmacos.cxx +++ b/vcl/osx/salmacos.cxx @@ -26,6 +26,7 @@ #include <osl/diagnose.h> #include <vcl/bitmap.hxx> +#include <vcl/skia/SkiaHelper.hxx> #include <quartz/salbmp.h> #include <quartz/salgdi.h> @@ -506,6 +507,12 @@ bool AquaSalVirtualDevice::SetSize(tools::Long nDX, tools::Long nDY) nFlags = uint32_t(kCGImageAlphaNoneSkipFirst) | uint32_t(kCGBitmapByteOrder32Host); } + if (SkiaHelper::isVCLSkiaEnabled()) + { + mpGraphics->SetVirDevGraphics(this, maLayer, nullptr, mnBitmapDepth); + return true; + } + // Allocate buffer for virtual device graphics as bitmap context to store graphics with highest required (scaled) resolution size_t nScaledWidth = mnWidth * fScale; diff --git a/vcl/skia/osx/gdiimpl.cxx b/vcl/skia/osx/gdiimpl.cxx index 9b511ad4469b..ffe1ebc42d65 100644 --- a/vcl/skia/osx/gdiimpl.cxx +++ b/vcl/skia/osx/gdiimpl.cxx @@ -37,6 +37,24 @@ using namespace SkiaHelper; +namespace +{ +struct SnapshotImageData +{ + sk_sp<SkImage> image; + SkPixmap pixmap; +}; +} + +static void SnapshotImageDataCallback(void* pInfo, const void* pData, size_t nSize) +{ + (void)pData; + (void)nSize; + + if (pInfo) + delete static_cast<SnapshotImageData*>(pInfo); +} + AquaSkiaSalGraphicsImpl::AquaSkiaSalGraphicsImpl(AquaSalGraphics& rParent, AquaSharedAttributes& rShared) : SkiaSalGraphicsImpl(rParent, rShared.mpFrame) @@ -101,117 +119,129 @@ void AquaSkiaSalGraphicsImpl::WindowBackingPropertiesChanged() { windowBackingPr void AquaSkiaSalGraphicsImpl::flushSurfaceToWindowContext() { if (!isGPU()) - flushSurfaceToScreenCG(); + { + // tdf159175 mark dirty area in NSWindow for redrawing + // This will cause -[SalFrameView drawRect:] to be called. That, + // in turn, will draw a CGImageRef of the surface fetched from + // AquaSkiaSalGraphicsImpl::createCGImageFromRasterSurface(). + mrShared.refreshRect(mDirtyRect.x(), mDirtyRect.y(), mDirtyRect.width(), + mDirtyRect.height()); + } else + { SkiaSalGraphicsImpl::flushSurfaceToWindowContext(); + } } // For Raster we use our own screen blitting (see above). -void AquaSkiaSalGraphicsImpl::flushSurfaceToScreenCG() +CGImageRef AquaSkiaSalGraphicsImpl::createCGImageFromRasterSurface(const NSRect& rDirtyRect, + CGPoint& rImageOrigin, + bool& rImageFlipped) { + if (isGPU() || !mSurface) + return nullptr; + // Based on AquaGraphicsBackend::drawBitmap(). if (!mrShared.checkContext()) - return; + return nullptr; + + NSRect aIntegralRect = NSIntegralRect(rDirtyRect); + if (NSIsEmptyRect(aIntegralRect)) + return nullptr; - assert(mSurface.get()); // Do not use sub-rect, it creates copies of the data. - sk_sp<SkImage> image = makeCheckedImageSnapshot(mSurface); - SkPixmap pixmap; - if (!image->peekPixels(&pixmap)) + SnapshotImageData* pInfo = new SnapshotImageData; + pInfo->image = makeCheckedImageSnapshot(mSurface); + if (!pInfo->image->peekPixels(&pInfo->pixmap)) abort(); - // If window scaling, then mDirtyRect is in VCL coordinates, mSurface has screen size (=points,HiDPI), - // maContextHolder has screen size but a scale matrix set so its inputs are in VCL coordinates (see - // its setup in AquaSharedAttributes::checkContext()). + + SkIRect aDirtyRect = SkIRect::MakeXYWH( + aIntegralRect.origin.x * mScaling, aIntegralRect.origin.y * mScaling, + aIntegralRect.size.width * mScaling, aIntegralRect.size.height * mScaling); + if (mrShared.isFlipped()) + aDirtyRect = SkIRect::MakeXYWH( + aDirtyRect.x(), pInfo->pixmap.bounds().height() - aDirtyRect.y() - aDirtyRect.height(), + aDirtyRect.width(), aDirtyRect.height()); + if (!aDirtyRect.intersect(pInfo->pixmap.bounds())) + { + delete pInfo; + return nullptr; + } + + // If window scaling, then aDirtyRect is in scaled VCL coordinates and mSurface has + // screen size (=points,HiDPI). // This creates the bitmap context from the cropped part, writable_addr32() will get - // the first pixel of mDirtyRect.topLeft(), and using pixmap.rowBytes() ensures the following + // the first pixel of aDirtyRect.topLeft(), and using pixmap.rowBytes() ensures the following // pixel lines will be read from correct positions. - if (pixmap.bounds() != mDirtyRect && pixmap.bounds().bottom() == mDirtyRect.bottom()) + if (pInfo->pixmap.bounds() != aDirtyRect + && pInfo->pixmap.bounds().bottom() == aDirtyRect.bottom()) { - // HACK for tdf#145843: If mDirtyRect includes the last line but not the first pixel of it, + // HACK for tdf#145843: If aDirtyRect includes the last line but not the first pixel of it, // then the rowBytes() trick would lead to the CG* functions thinking that even pixels after // the pixmap data belong to the area (since the shifted x()+rowBytes() points there) and // at least on Intel Mac they would actually read those data, even though I see no good reason // to do that, as that's beyond the x()+width() for the last line. That could be handled // by creating a subset SkImage (which as is said above copies data), or set the x coordinate // to 0, which will then make rowBytes() match the actual data. - mDirtyRect.fLeft = 0; + aDirtyRect.fLeft = 0; // Related tdf#156630 pixmaps can be wider than the dirty rectangle // This seems to most commonly occur when SAL_FORCE_HIDPI_SCALING=1 // and the native window scale is 2. - assert(mDirtyRect.width() <= pixmap.bounds().width()); + assert(aDirtyRect.width() <= pInfo->pixmap.bounds().width()); } // tdf#145843 Do not use CGBitmapContextCreate() to create a bitmap context // As described in the comment in the above code, CGBitmapContextCreate() // and CGBitmapContextCreateWithData() will try to access pixels up to - // mDirtyRect.x() + pixmap.bounds.width() for each row. When reading the + // aDirtyRect.x() + pixmap.bounds.width() for each row. When reading the // last line in the SkPixmap, the buffer allocated for the SkPixmap ends at - // mDirtyRect.x() + mDirtyRect.width() and mDirtyRect.width() is clamped to - // pixmap.bounds.width() - mDirtyRect.x(). + // aDirtyRect.x() + aDirtyRect.width() and aDirtyRect.width() is clamped to + // pixmap.bounds.width() - aDirtyRect.x(). // This behavior looks like an optimization within CGBitmapContextCreate() // to draw with a single memcpy() so fix this bug by chaining the // CGDataProvider(), CGImageCreate(), and CGImageCreateWithImageInRect() // functions to create the screen image. - CGDataProviderRef dataProvider = CGDataProviderCreateWithData( - nullptr, pixmap.writable_addr32(0, 0), pixmap.computeByteSize(), nullptr); + CGDataProviderRef dataProvider + = CGDataProviderCreateWithData(pInfo, pInfo->pixmap.writable_addr32(0, 0), + pInfo->pixmap.computeByteSize(), SnapshotImageDataCallback); if (!dataProvider) { + delete pInfo; SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate data provider"); - return; + return nullptr; } - CGImageRef fullImage = CGImageCreate(pixmap.bounds().width(), pixmap.bounds().height(), 8, - 8 * image->imageInfo().bytesPerPixel(), pixmap.rowBytes(), - GetSalData()->mxRGBSpace, - SkiaToCGBitmapType(image->colorType(), image->alphaType()), - dataProvider, nullptr, false, kCGRenderingIntentDefault); + CGImageRef fullImage + = CGImageCreate(pInfo->pixmap.bounds().width(), pInfo->pixmap.bounds().height(), 8, + 8 * pInfo->image->imageInfo().bytesPerPixel(), pInfo->pixmap.rowBytes(), + GetSalData()->mxRGBSpace, + SkiaToCGBitmapType(pInfo->image->colorType(), pInfo->image->alphaType()), + dataProvider, nullptr, false, kCGRenderingIntentDefault); if (!fullImage) { CGDataProviderRelease(dataProvider); SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate full image"); - return; + return nullptr; } CGImageRef screenImage = CGImageCreateWithImageInRect( - fullImage, CGRectMake(mDirtyRect.x() * mScaling, mDirtyRect.y() * mScaling, - mDirtyRect.width() * mScaling, mDirtyRect.height() * mScaling)); + fullImage, + CGRectMake(aDirtyRect.x(), aDirtyRect.y(), aDirtyRect.width(), aDirtyRect.height())); if (!screenImage) { CGImageRelease(fullImage); CGDataProviderRelease(dataProvider); - SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate screen image"); - return; + SAL_WARN("vcl.skia", "createCGImageFromRasterSurface(): Failed to allocate screen image"); + return nullptr; } - mrShared.maContextHolder.saveState(); - // Drawing to the actual window has scaling active, so use unscaled coordinates, the scaling matrix will scale them - // to the proper screen coordinates. Unless the scaling is fake for debugging, in which case scale them to draw - // at the scaled size. - int windowScaling = 1; - static const char* env = getenv("SAL_FORCE_HIDPI_SCALING"); - if (env != nullptr) - windowScaling = atoi(env); - CGRect drawRect - = CGRectMake(mDirtyRect.x() * windowScaling, mDirtyRect.y() * windowScaling, - mDirtyRect.width() * windowScaling, mDirtyRect.height() * windowScaling); - if (mrShared.isFlipped()) - { - // I don't understand why, but apparently it's needed to explicitly to flip the drawing, even though maContextHelper - // has this set up, so this unsets the flipping. - CGFloat invertedY = drawRect.origin.y + drawRect.size.height; - CGContextTranslateCTM(mrShared.maContextHolder.get(), 0, invertedY); - CGContextScaleCTM(mrShared.maContextHolder.get(), 1, -1); - drawRect.origin.y = 0; - } - CGContextDrawImage(mrShared.maContextHolder.get(), drawRect, screenImage); - mrShared.maContextHolder.restoreState(); + rImageOrigin = CGPointMake(aDirtyRect.x(), aDirtyRect.y()); + rImageFlipped = mrShared.isFlipped(); - CGImageRelease(screenImage); CGImageRelease(fullImage); CGDataProviderRelease(dataProvider); - // This is also in VCL coordinates. - mrShared.refreshRect(mDirtyRect.x(), mDirtyRect.y(), mDirtyRect.width(), mDirtyRect.height()); + return screenImage; } bool AquaSkiaSalGraphicsImpl::drawNativeControl(ControlType nType, ControlPart nPart,