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 <guibomacdev@gmail.com>
(cherry picked from commit 12dbf0e6b6b485a1d73c7e33bd0ecfb13e6efdac)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/169559
Reviewed-by: Christian Lohmaier <lohmaier+LibreOffice@googlemail.com>
diff --git a/vcl/inc/skia/osx/gdiimpl.hxx b/vcl/inc/skia/osx/gdiimpl.hxx
index b97245e..b60280a 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 a944529..d7f8ec4 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 700b252..14e2a80 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 9b511ad..ffe1ebc 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;

    assert(mSurface.get());
    NSRect aIntegralRect = NSIntegralRect(rDirtyRect);
    if (NSIsEmptyRect(aIntegralRect))
        return nullptr;

    // 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()).
    // 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
    // pixel lines will be read from correct positions.
    if (pixmap.bounds() != mDirtyRect && pixmap.bounds().bottom() == mDirtyRect.bottom())

    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()))
    {
        // HACK for tdf#145843: If mDirtyRect includes the last line but not the first pixel of it,
        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 aDirtyRect.topLeft(), and using pixmap.rowBytes() ensures the following
    // pixel lines will be read from correct positions.
    if (pInfo->pixmap.bounds() != aDirtyRect
        && pInfo->pixmap.bounds().bottom() == aDirtyRect.bottom())
    {
        // 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,