batch Skia xor drawing (tdf#132241)

The code previously applied the xor operation after each drawing,
but the bugdoc draws a large number of polygons in xor mode,
so the xor drawing was done repeatedly. Do the xor drawing just
once when leaving the xor mode.

Change-Id: I6c8d1c2f01688dc957a0af75232ee9fb69fe5d1b
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/92558
Tested-by: Jenkins
Reviewed-by: Luboš Luňák <l.lunak@collabora.com>
diff --git a/vcl/inc/skia/gdiimpl.hxx b/vcl/inc/skia/gdiimpl.hxx
index a99bb9a..9040532 100644
--- a/vcl/inc/skia/gdiimpl.hxx
+++ b/vcl/inc/skia/gdiimpl.hxx
@@ -26,6 +26,7 @@
#include <salgeom.hxx>

#include <SkSurface.h>
#include <SkRegion.h>

#include <prewin.h>
#include <tools/sk_app/WindowContext.h>
@@ -219,6 +220,8 @@
    void postDraw();
    // The canvas to draw to. Will be diverted to a temporary for Xor mode.
    SkCanvas* getDrawCanvas() { return mXorMode ? getXorCanvas() : mSurface->getCanvas(); }
    // Call before makeImageSnapshot(), ensures the content is up to date.
    void flushDrawing();

    virtual void createSurface();
    // Call to ensure that mSurface is valid. If mSurface is going to be modified,
@@ -254,6 +257,15 @@
    void drawMask(const SalTwoRect& rPosAry, const sk_sp<SkImage>& rImage, Color nMaskColor);

    SkCanvas* getXorCanvas();
    void applyXor();
    void addXorRegion(const SkRect& rect)
    {
        if (mXorMode)
        {
            // Make slightly larger, just in case (rounding, antialiasing,...).
            mXorRegion.op(rect.makeOutset(2, 2).round(), SkRegion::kUnion_Op);
        }
    }
    static void setCanvasClipRegion(SkCanvas* canvas, const vcl::Region& region);

    // When drawing using GPU, rounding errors may result in off-by-one errors,
@@ -286,7 +298,7 @@
    bool mXorMode;
    SkBitmap mXorBitmap;
    std::unique_ptr<SkCanvas> mXorCanvas;
    SkRect mXorExtents; // the area that needs updating for the xor operation (or empty for all)
    SkRegion mXorRegion; // the area that needs updating for the xor operation
    std::unique_ptr<SkiaFlushIdle> mFlush;
    int mPendingPixelsToFlush;
};
diff --git a/vcl/inc/skia/utils.hxx b/vcl/inc/skia/utils.hxx
index 16e5add..942b5c3 100644
--- a/vcl/inc/skia/utils.hxx
+++ b/vcl/inc/skia/utils.hxx
@@ -25,6 +25,7 @@
#include <tools/gen.hxx>
#include <driverblocklist.hxx>

#include <SkRegion.h>
#include <tools/sk_app/VulkanWindowContext.h>

namespace SkiaHelper
@@ -67,6 +68,42 @@

} // namespace

template <typename charT, typename traits>
inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& stream,
                                                     const SkRect& rectangle)
{
    if (rectangle.isEmpty())
        return stream << "EMPTY";
    else
        return stream << rectangle.width() << 'x' << rectangle.height() << "@(" << rectangle.x()
                      << ',' << rectangle.y() << ")";
}

template <typename charT, typename traits>
inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& stream,
                                                     const SkIRect& rectangle)
{
    if (rectangle.isEmpty())
        return stream << "EMPTY";
    else
        return stream << rectangle.width() << 'x' << rectangle.height() << "@(" << rectangle.x()
                      << ',' << rectangle.y() << ")";
}

template <typename charT, typename traits>
inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& stream,
                                                     const SkRegion& region)
{
    if (region.isEmpty())
        return stream << "EMPTY";
    stream << "(";
    SkRegion::Iterator it(region);
    for (int i = 0; !it.done(); it.next(), ++i)
        stream << "[" << i << "] " << it.rect();
    stream << ")";
    return stream;
}

#endif // INCLUDED_VCL_INC_SKIA_UTILS_H

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/gdiimpl.cxx b/vcl/skia/gdiimpl.cxx
index b4bda61..1937115 100644
--- a/vcl/skia/gdiimpl.cxx
+++ b/vcl/skia/gdiimpl.cxx
@@ -327,71 +327,10 @@
    assert(comphelper::SolarMutex::get()->IsCurrentThread());
    SkiaZone::enter(); // matched in postDraw()
    checkSurface();
    assert(!mXorMode || mXorExtents.isEmpty()); // must be reset in postDraw()
}

void SkiaSalGraphicsImpl::postDraw()
{
    if (mXorMode)
    {
        // Apply the result from the temporary bitmap manually. This is indeed
        // slow, but it doesn't seem to be needed often and can be optimized
        // in each operation by setting mXorExtents to the area that should be
        // updated.
        if (mXorExtents.isEmpty())
            mXorExtents = SkRect::MakeXYWH(0, 0, mSurface->width(), mSurface->height());
        else
        {
            // Make slightly larger, just in case (rounding, antialiasing,...).
            mXorExtents.outset(2, 2);
            if (!mXorExtents.intersect(
                    SkRect::MakeXYWH(0, 0, mSurface->width(), mSurface->height())))
                mXorExtents.setEmpty();
        }
        SAL_INFO("vcl.skia.trace",
                 "applyxor(" << this << "): "
                             << tools::Rectangle(mXorExtents.left(), mXorExtents.top(),
                                                 mXorExtents.right(), mXorExtents.bottom()));
        if (!mXorExtents.isEmpty()) // the intersection above may be empty
        {
            // Copy the surface contents to another pixmap.
            SkBitmap surfaceBitmap;
            // Use unpremultiplied alpha format, so that we do not have to do the conversions to get
            // the RGB and back (Skia will do it when converting, but it'll be presumably faster at it).
            if (!surfaceBitmap.tryAllocPixels(
                    mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType)))
                abort();
            SkPaint paint;
            paint.setBlendMode(SkBlendMode::kSrc); // copy as is
            SkCanvas canvas(surfaceBitmap);
            canvas.drawImageRect(mSurface->makeImageSnapshot(), mXorExtents, mXorExtents, &paint);
            // xor to surfaceBitmap
            assert(surfaceBitmap.info().alphaType() == kUnpremul_SkAlphaType);
            assert(mXorBitmap.info().alphaType() == kUnpremul_SkAlphaType);
            assert(surfaceBitmap.bytesPerPixel() == 4);
            assert(mXorBitmap.bytesPerPixel() == 4);
            for (int y = mXorExtents.top(); y < mXorExtents.bottom(); ++y)
            {
                uint8_t* data = static_cast<uint8_t*>(surfaceBitmap.getAddr(mXorExtents.x(), y));
                const uint8_t* xordata
                    = static_cast<uint8_t*>(mXorBitmap.getAddr(mXorExtents.x(), y));
                for (int x = 0; x < mXorExtents.width(); ++x)
                {
                    *data++ ^= *xordata++;
                    *data++ ^= *xordata++;
                    *data++ ^= *xordata++;
                    // alpha is not xor-ed
                    data++;
                    xordata++;
                }
            }
            surfaceBitmap.notifyPixelsChanged();
            mSurface->getCanvas()->drawBitmapRect(surfaceBitmap, mXorExtents, mXorExtents, &paint);
        }
        mXorCanvas.reset();
        mXorBitmap.reset();
        mXorExtents.setEmpty();
    }
    if (!isOffscreen())
    {
        if (!Application::IsInExecute())
@@ -440,7 +379,10 @@
            // will be usually repainted anyway.
            sk_sp<SkImage> snapshot;
            if (!isOffscreen())
            {
                flushDrawing();
                snapshot = mSurface->makeImageSnapshot();
            }
            recreateSurface();
            if (snapshot)
            {
@@ -456,6 +398,13 @@
    }
}

void SkiaSalGraphicsImpl::flushDrawing()
{
    if (mXorMode)
        applyXor();
    mSurface->flush();
}

bool SkiaSalGraphicsImpl::setClipRegion(const vcl::Region& region)
{
    if (mClipRegion == region)
@@ -520,9 +469,14 @@

void SkiaSalGraphicsImpl::SetXORMode(bool set, bool)
{
    if (mXorMode == set)
        return;
    SAL_INFO("vcl.skia.trace", "setxormode(" << this << "): " << set);
    if (set)
        mXorRegion.setEmpty();
    else
        applyXor();
    mXorMode = set;
    if (mXorMode)
        mXorExtents.setEmpty();
}

SkCanvas* SkiaSalGraphicsImpl::getXorCanvas()
@@ -534,7 +488,7 @@
    // There's no point in using SkSurface for GPU, we'd immediately need to get the pixels back.
    if (!mXorCanvas)
    {
        // Use unpremultiplied alpha (see xor applying in PostDraw()).
        // Use unpremultiplied alpha (see xor applying in applyXor()).
        if (!mXorBitmap.tryAllocPixels(mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType)))
            abort();
        mXorBitmap.eraseARGB(0, 0, 0, 0);
@@ -544,6 +498,61 @@
    return mXorCanvas.get();
}

void SkiaSalGraphicsImpl::applyXor()
{
    // Apply the result from the temporary bitmap manually. This is indeed
    // slow, but it doesn't seem to be needed often and is optimized
    // in each operation by extending mXorRegion with the area that should be
    // updated.
    assert(mXorMode);
    if (!mXorRegion.op(SkIRect::MakeXYWH(0, 0, mSurface->width(), mSurface->height()),
                       SkRegion::kIntersect_Op))
    {
        mXorRegion.setEmpty();
        return;
    }
    SAL_INFO("vcl.skia.trace", "applyxor(" << this << "): " << mXorRegion);
    // Copy the surface contents to another pixmap.
    SkBitmap surfaceBitmap;
    // Use unpremultiplied alpha format, so that we do not have to do the conversions to get
    // the RGB and back (Skia will do it when converting, but it'll be presumably faster at it).
    if (!surfaceBitmap.tryAllocPixels(mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType)))
        abort();
    SkPaint paint;
    paint.setBlendMode(SkBlendMode::kSrc); // copy as is
    SkCanvas canvas(surfaceBitmap);
    canvas.drawImageRect(mSurface->makeImageSnapshot(), mXorRegion.getBounds(),
                         SkRect::Make(mXorRegion.getBounds()), &paint);
    // xor to surfaceBitmap
    assert(surfaceBitmap.info().alphaType() == kUnpremul_SkAlphaType);
    assert(mXorBitmap.info().alphaType() == kUnpremul_SkAlphaType);
    assert(surfaceBitmap.bytesPerPixel() == 4);
    assert(mXorBitmap.bytesPerPixel() == 4);
    for (SkRegion::Iterator it(mXorRegion); !it.done(); it.next())
    {
        for (int y = it.rect().top(); y < it.rect().bottom(); ++y)
        {
            uint8_t* data = static_cast<uint8_t*>(surfaceBitmap.getAddr(it.rect().x(), y));
            const uint8_t* xordata = static_cast<uint8_t*>(mXorBitmap.getAddr(it.rect().x(), y));
            for (int x = 0; x < it.rect().width(); ++x)
            {
                *data++ ^= *xordata++;
                *data++ ^= *xordata++;
                *data++ ^= *xordata++;
                // alpha is not xor-ed
                data++;
                xordata++;
            }
        }
    }
    surfaceBitmap.notifyPixelsChanged();
    mSurface->getCanvas()->drawBitmapRect(surfaceBitmap, mXorRegion.getBounds(),
                                          SkRect::Make(mXorRegion.getBounds()), &paint);
    mXorCanvas.reset();
    mXorBitmap.reset();
    mXorRegion.setEmpty();
}

void SkiaSalGraphicsImpl::SetROPLineColor(SalROPColor nROPColor)
{
    switch (nROPColor)
@@ -589,8 +598,7 @@
    // Apparently drawPixel() is actually expected to set the pixel and not draw it.
    paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
    getDrawCanvas()->drawPoint(toSkX(nX), toSkY(nY), paint);
    if (mXorMode) // limit xor area update
        mXorExtents = SkRect::MakeXYWH(nX, nY, 1, 1);
    addXorRegion(SkRect::MakeXYWH(nX, nY, 1, 1));
    postDraw();
}

@@ -609,8 +617,7 @@
        getDrawCanvas()->drawLine(nX1 + 0.25, nY1 + 0.25, nX2 + 0.25, nY2 + 0.25, paint);
    else
        getDrawCanvas()->drawLine(toSkX(nX1), toSkY(nY1), toSkX(nX2), toSkY(nY2), paint);
    if (mXorMode) // limit xor area update
        mXorExtents = SkRect::MakeLTRB(nX1, nY1, nX2 + 1, nY2 + 1);
    addXorRegion(SkRect::MakeLTRB(nX1, nY1, nX2 + 1, nY2 + 1));
    postDraw();
}

@@ -636,8 +643,7 @@
        paint.setStyle(SkPaint::kStroke_Style);
        canvas->drawIRect(SkIRect::MakeXYWH(nX, nY, nWidth - 1, nHeight - 1), paint);
    }
    if (mXorMode) // limit xor area update
        mXorExtents = SkRect::MakeXYWH(nX, nY, nWidth, nHeight);
    addXorRegion(SkRect::MakeXYWH(nX, nY, nWidth, nHeight));
    postDraw();
}

@@ -732,8 +738,7 @@
        aPaint.setStyle(SkPaint::kStroke_Style);
        getDrawCanvas()->drawPath(aPath, aPaint);
    }
    if (mXorMode) // limit xor area update
        mXorExtents = aPath.getBounds();
    addXorRegion(aPath.getBounds());
    postDraw();
#if defined LINUX
    // WORKAROUND: The logo in the about dialog has drawing errors. This seems to happen
@@ -861,8 +866,7 @@
    // case as it seems to produce better results.
    aPath.offset(0.5, 0.5, nullptr);
    getDrawCanvas()->drawPath(aPath, aPaint);
    if (mXorMode) // limit xor area update
        mXorExtents = aPath.getBounds();
    addXorRegion(aPath.getBounds());
    postDraw();

    return true;
@@ -910,9 +914,9 @@
    SAL_INFO("vcl.skia.trace", "copyarea(" << this << "): " << Point(nSrcX, nSrcY) << "->"
                                           << Point(nDestX, nDestY) << "/"
                                           << Size(nSrcWidth, nSrcHeight));
    assert(!mXorMode);
    ::copyArea(getDrawCanvas(), mSurface, nDestX, nDestY, nSrcX, nSrcY, nSrcWidth, nSrcHeight);
    if (mXorMode) // limit xor area update
        mXorExtents = SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight);
    addXorRegion(SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight));
    postDraw();
}

@@ -925,9 +929,13 @@
        assert(dynamic_cast<SkiaSalGraphicsImpl*>(pSrcGraphics->GetImpl()));
        src = static_cast<SkiaSalGraphicsImpl*>(pSrcGraphics->GetImpl());
        src->checkSurface();
        src->flushDrawing();
    }
    else
    {
        src = this;
        assert(!mXorMode);
    }
    if (rPosAry.mnSrcWidth == rPosAry.mnDestWidth && rPosAry.mnSrcHeight == rPosAry.mnDestHeight)
    {
        auto srcDebug = [&]() -> std::string {
@@ -959,9 +967,8 @@
                                                        rPosAry.mnDestWidth, rPosAry.mnDestHeight),
                                       &paint);
    }
    if (mXorMode) // limit xor area update
        mXorExtents = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
                                       rPosAry.mnDestHeight);
    addXorRegion(SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
                                  rPosAry.mnDestHeight));
    postDraw();
}

@@ -1078,7 +1085,7 @@
    checkSurface();
    SAL_INFO("vcl.skia.trace",
             "getbitmap(" << this << "): " << Point(nX, nY) << "/" << Size(nWidth, nHeight));
    mSurface->getCanvas()->flush();
    flushDrawing();
    // TODO makeImageSnapshot(rect) may copy the data, which may be a waste if this is used
    // e.g. for VirtualDevice's lame alpha blending, in which case the image will eventually end up
    // in blendAlphaBitmap(), where we could simply use the proper rect of the image.
@@ -1105,6 +1112,7 @@
{
    preDraw();
    SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags));
    assert(!mXorMode);
    // Intel Vulkan drivers (up to current 0.401.3889) have a problem
    // with SkBlendMode::kDifference(?) and surfaces wider than 1024 pixels, resulting
    // in drawing errors. Work that around by fetching the relevant part of the surface
@@ -1139,13 +1147,12 @@
            sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(area.width(), area.height());
            SkPaint copy;
            copy.setBlendMode(SkBlendMode::kSrc);
            flushDrawing();
            surface->getCanvas()->drawImageRect(mSurface->makeImageSnapshot(), area, size, &copy);
            aPath.offset(-area.x(), -area.y());
            surface->getCanvas()->drawPath(aPath, aPaint);
            getDrawCanvas()->drawImageRect(surface->makeImageSnapshot(), size, area, &copy);
        }
        if (mXorMode) // limit xor area update
            mXorExtents = aPath.getBounds();
    }
    else
    {
@@ -1188,13 +1195,13 @@
            sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(area.width(), area.height());
            SkPaint copy;
            copy.setBlendMode(SkBlendMode::kSrc);
            flushDrawing();
            surface->getCanvas()->drawImageRect(mSurface->makeImageSnapshot(), area, size, &copy);
            aPath.offset(-area.x(), -area.y());
            surface->getCanvas()->drawPath(aPath, aPaint);
            getDrawCanvas()->drawImageRect(surface->makeImageSnapshot(), size, area, &copy);
        }
        if (mXorMode) // limit xor area update
            mXorExtents = aPath.getBounds();
        addXorRegion(aPath.getBounds());
    }
    postDraw();
}
@@ -1258,8 +1265,7 @@
    preDraw();
    SAL_INFO("vcl.skia.trace", "drawimage(" << this << "): " << rPosAry << ":" << int(eBlendMode));
    getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect, &aPaint);
    if (mXorMode) // limit xor area update
        mXorExtents = aDestinationRect;
    addXorRegion(aDestinationRect);
    postDraw();
}

@@ -1277,8 +1283,7 @@
    preDraw();
    SAL_INFO("vcl.skia.trace", "drawbitmap(" << this << "): " << rPosAry << ":" << int(eBlendMode));
    getDrawCanvas()->drawBitmapRect(aBitmap, aSourceRect, aDestinationRect, &aPaint);
    if (mXorMode) // limit xor area update
        mXorExtents = aDestinationRect;
    addXorRegion(aDestinationRect);
    mPendingPixelsToFlush += aBitmap.width() * aBitmap.height();
    postDraw();
}
@@ -1332,6 +1337,7 @@
        getDrawCanvas()->concat(aMatrix);
        getDrawCanvas()->drawImage(tmpSurface->makeImageSnapshot(), 0, 0);
    }
    assert(!mXorMode);
    postDraw();

    return true;
@@ -1385,14 +1391,11 @@
                                      glyphForms.data(), font, SkTextEncoding::kGlyphID);
    preDraw();
    SAL_INFO("vcl.skia.trace",
             "drawtextblob(" << this << "): "
                             << tools::Rectangle(
                                    Point(textBlob->bounds().x(), textBlob->bounds().y()),
                                    Size(textBlob->bounds().width(), textBlob->bounds().height()))
                             << ":" << textColor);
             "drawtextblob(" << this << "): " << textBlob->bounds() << ":" << textColor);
    SkPaint paint;
    paint.setColor(toSkColor(textColor));
    getDrawCanvas()->drawTextBlob(textBlob, 0, 0, paint);
    addXorRegion(textBlob->bounds());
    postDraw();
}