summaryrefslogtreecommitdiffstats
path: root/drawinglayer/source/processor2d/vclhelperbufferdevice.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'drawinglayer/source/processor2d/vclhelperbufferdevice.cxx')
-rw-r--r--drawinglayer/source/processor2d/vclhelperbufferdevice.cxx593
1 files changed, 593 insertions, 0 deletions
diff --git a/drawinglayer/source/processor2d/vclhelperbufferdevice.cxx b/drawinglayer/source/processor2d/vclhelperbufferdevice.cxx
new file mode 100644
index 0000000000..28d383230e
--- /dev/null
+++ b/drawinglayer/source/processor2d/vclhelperbufferdevice.cxx
@@ -0,0 +1,593 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <sal/config.h>
+#include <sal/log.hxx>
+
+#include <algorithm>
+#include <map>
+#include <vector>
+
+#include "vclhelperbufferdevice.hxx"
+#include <basegfx/range/b2drange.hxx>
+#include <vcl/bitmapex.hxx>
+#include <basegfx/matrix/b2dhommatrix.hxx>
+#include <basegfx/matrix/b2dhommatrixtools.hxx>
+#include <vcl/timer.hxx>
+#include <vcl/lazydelete.hxx>
+#include <vcl/dibtools.hxx>
+#include <vcl/skia/SkiaHelper.hxx>
+#include <mutex>
+
+#ifdef DBG_UTIL
+#include <tools/stream.hxx>
+#endif
+
+// #define SPEED_COMPARE
+#ifdef SPEED_COMPARE
+#include <tools/time.hxx>
+#endif
+
+// buffered VDev usage
+namespace
+{
+class VDevBuffer : public Timer
+{
+private:
+ struct Entry
+ {
+ VclPtr<VirtualDevice> buf;
+ Entry(const VclPtr<VirtualDevice>& vdev)
+ : buf(vdev)
+ {
+ }
+ };
+
+ std::mutex m_aMutex;
+
+ // available buffers
+ std::vector<Entry> maFreeBuffers;
+
+ // allocated/used buffers (remembered to allow deleting them in destructor)
+ std::vector<Entry> maUsedBuffers;
+
+ // remember what outputdevice was the template passed to VirtualDevice::Create
+ // so we can test if that OutputDevice was disposed before reusing a
+ // virtualdevice because that isn't safe to do at least for Gtk2
+ std::map<VclPtr<VirtualDevice>, VclPtr<OutputDevice>> maDeviceTemplates;
+
+ static bool isSizeSuitable(const VclPtr<VirtualDevice>& device, const Size& size);
+
+public:
+ VDevBuffer();
+ virtual ~VDevBuffer() override;
+
+ VclPtr<VirtualDevice> alloc(OutputDevice& rOutDev, const Size& rSizePixel);
+ void free(VirtualDevice& rDevice);
+
+ // Timer virtuals
+ virtual void Invoke() override;
+};
+
+VDevBuffer::VDevBuffer()
+ : Timer("drawinglayer::VDevBuffer via Invoke()")
+{
+ SetTimeout(10L * 1000L); // ten seconds
+}
+
+VDevBuffer::~VDevBuffer()
+{
+ std::unique_lock aGuard(m_aMutex);
+ Stop();
+
+ while (!maFreeBuffers.empty())
+ {
+ maFreeBuffers.back().buf.disposeAndClear();
+ maFreeBuffers.pop_back();
+ }
+
+ while (!maUsedBuffers.empty())
+ {
+ maUsedBuffers.back().buf.disposeAndClear();
+ maUsedBuffers.pop_back();
+ }
+}
+
+bool VDevBuffer::isSizeSuitable(const VclPtr<VirtualDevice>& device, const Size& rSizePixel)
+{
+ if (device->GetOutputWidthPixel() >= rSizePixel.getWidth()
+ && device->GetOutputHeightPixel() >= rSizePixel.getHeight())
+ {
+ bool requireSmall = false;
+#if defined(UNX)
+ // HACK: See the small size handling in SvpSalVirtualDevice::CreateSurface().
+ // Make sure to not reuse a larger device when a small one should be preferred.
+ if (device->GetRenderBackendName() == "svp")
+ requireSmall = true;
+#endif
+ // The same for Skia, see renderMethodToUseForSize().
+ if (SkiaHelper::isVCLSkiaEnabled())
+ requireSmall = true;
+ if (requireSmall)
+ {
+ if (rSizePixel.getWidth() <= 32 && rSizePixel.getHeight() <= 32
+ && (device->GetOutputWidthPixel() > 32 || device->GetOutputHeightPixel() > 32))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+VclPtr<VirtualDevice> VDevBuffer::alloc(OutputDevice& rOutDev, const Size& rSizePixel)
+{
+ std::unique_lock aGuard(m_aMutex);
+ VclPtr<VirtualDevice> pRetval;
+
+ sal_Int32 nBits = rOutDev.GetBitCount();
+
+ bool bOkay(false);
+ if (!maFreeBuffers.empty())
+ {
+ auto aFound(maFreeBuffers.end());
+
+ for (auto a = maFreeBuffers.begin(); a != maFreeBuffers.end(); ++a)
+ {
+ assert(a->buf && "Empty pointer in VDevBuffer (!)");
+
+ if (nBits == a->buf->GetBitCount())
+ {
+ // candidate is valid due to bit depth
+ if (aFound != maFreeBuffers.end())
+ {
+ // already found
+ if (bOkay)
+ {
+ // found is valid
+ const bool bCandidateOkay = isSizeSuitable(a->buf, rSizePixel);
+
+ if (bCandidateOkay)
+ {
+ // found and candidate are valid
+ const sal_uLong aSquare(aFound->buf->GetOutputWidthPixel()
+ * aFound->buf->GetOutputHeightPixel());
+ const sal_uLong aCandidateSquare(a->buf->GetOutputWidthPixel()
+ * a->buf->GetOutputHeightPixel());
+
+ if (aCandidateSquare < aSquare)
+ {
+ // candidate is valid and smaller, use it
+ aFound = a;
+ }
+ }
+ else
+ {
+ // found is valid, candidate is not. Keep found
+ }
+ }
+ else
+ {
+ // found is invalid, use candidate
+ aFound = a;
+ bOkay = isSizeSuitable(aFound->buf, rSizePixel);
+ }
+ }
+ else
+ {
+ // none yet, use candidate
+ aFound = a;
+ bOkay = isSizeSuitable(aFound->buf, rSizePixel);
+ }
+ }
+ }
+
+ if (aFound != maFreeBuffers.end())
+ {
+ pRetval = aFound->buf;
+ maFreeBuffers.erase(aFound);
+ }
+ }
+
+ if (pRetval)
+ {
+ // found a suitable cached virtual device, but the
+ // outputdevice it was based on has been disposed,
+ // drop it and create a new one instead as reusing
+ // such devices is unsafe under at least Gtk2
+ if (maDeviceTemplates[pRetval]->isDisposed())
+ {
+ maDeviceTemplates.erase(pRetval);
+ pRetval.disposeAndClear();
+ }
+ else
+ {
+ if (bOkay)
+ {
+ pRetval->Erase(pRetval->PixelToLogic(
+ tools::Rectangle(0, 0, rSizePixel.getWidth(), rSizePixel.getHeight())));
+ }
+ else
+ {
+ pRetval->SetOutputSizePixel(rSizePixel, true);
+ }
+ }
+ }
+
+ // no success yet, create new buffer
+ if (!pRetval)
+ {
+ pRetval = VclPtr<VirtualDevice>::Create(rOutDev, DeviceFormat::WITHOUT_ALPHA);
+ maDeviceTemplates[pRetval] = &rOutDev;
+ pRetval->SetOutputSizePixel(rSizePixel, true);
+ }
+ else
+ {
+ // reused, reset some values
+ pRetval->SetMapMode();
+ pRetval->SetRasterOp(RasterOp::OverPaint);
+ }
+
+ // remember allocated buffer
+ maUsedBuffers.emplace_back(pRetval);
+
+ return pRetval;
+}
+
+void VDevBuffer::free(VirtualDevice& rDevice)
+{
+ std::unique_lock aGuard(m_aMutex);
+ const auto aUsedFound
+ = std::find_if(maUsedBuffers.begin(), maUsedBuffers.end(),
+ [&rDevice](const Entry& el) { return el.buf == &rDevice; });
+ SAL_WARN_IF(aUsedFound == maUsedBuffers.end(), "drawinglayer",
+ "OOps, non-registered buffer freed (!)");
+ if (aUsedFound != maUsedBuffers.end())
+ {
+ maFreeBuffers.emplace_back(*aUsedFound);
+ maUsedBuffers.erase(aUsedFound);
+ SAL_WARN_IF(maFreeBuffers.size() > 1000, "drawinglayer",
+ "excessive cached buffers, " << maFreeBuffers.size() << " entries!");
+ }
+ Start();
+}
+
+void VDevBuffer::Invoke()
+{
+ std::unique_lock aGuard(m_aMutex);
+
+ while (!maFreeBuffers.empty())
+ {
+ auto aLastOne = maFreeBuffers.back();
+ maDeviceTemplates.erase(aLastOne.buf);
+ aLastOne.buf.disposeAndClear();
+ maFreeBuffers.pop_back();
+ }
+}
+
+#ifdef SPEED_COMPARE
+void doSpeedCompare(double fTrans, const Bitmap& rContent, const tools::Rectangle& rDestPixel,
+ OutputDevice& rOutDev)
+{
+ const int nAvInd(500);
+ static double fFactors[nAvInd];
+ static int nIndex(nAvInd + 1);
+ static int nRepeat(5);
+ static int nWorseTotal(0);
+ static int nBetterTotal(0);
+ int a(0);
+
+ const Size aSizePixel(rDestPixel.GetSize());
+
+ // init statics
+ if (nIndex > nAvInd)
+ {
+ for (a = 0; a < nAvInd; a++)
+ fFactors[a] = 1.0;
+ nIndex = 0;
+ }
+
+ // get start time
+ const sal_uInt64 nTimeA(tools::Time::GetSystemTicks());
+
+ // loop nRepeat times to get somewhat better timings, else
+ // numbers are pretty small
+ for (a = 0; a < nRepeat; a++)
+ {
+ // "Former" method using a temporary AlphaMask & DrawBitmapEx
+ sal_uInt8 nMaskValue(static_cast<sal_uInt8>(basegfx::fround(fTrans * 255.0)));
+ const AlphaMask aAlphaMask(aSizePixel, &nMaskValue);
+ rOutDev.DrawBitmapEx(rDestPixel.TopLeft(), BitmapEx(rContent, aAlphaMask));
+ }
+
+ // get intermediate time
+ const sal_uInt64 nTimeB(tools::Time::GetSystemTicks());
+
+ // loop nRepeat times
+ for (a = 0; a < nRepeat; a++)
+ {
+ // New method using DrawTransformedBitmapEx & fTrans directly
+ rOutDev.DrawTransformedBitmapEx(basegfx::utils::createScaleTranslateB2DHomMatrix(
+ aSizePixel.Width(), aSizePixel.Height(),
+ rDestPixel.TopLeft().X(), rDestPixel.TopLeft().Y()),
+ BitmapEx(rContent), 1 - fTrans);
+ }
+
+ // get end time
+ const sal_uInt64 nTimeC(tools::Time::GetSystemTicks());
+
+ // calculate deltas
+ const sal_uInt64 nTimeFormer(nTimeB - nTimeA);
+ const sal_uInt64 nTimeNew(nTimeC - nTimeB);
+
+ // compare & note down
+ if (nTimeFormer != nTimeNew && 0 != nTimeFormer && 0 != nTimeNew)
+ {
+ if ((nTimeFormer < 10 || nTimeNew < 10) && nRepeat < 500)
+ {
+ nRepeat += 1;
+ SAL_INFO("drawinglayer.processor2d", "Increment nRepeat to " << nRepeat);
+ return;
+ }
+
+ const double fNewFactor((double)nTimeFormer / nTimeNew);
+ fFactors[nIndex % nAvInd] = fNewFactor;
+ nIndex++;
+ double fAverage(0.0);
+ {
+ for (a = 0; a < nAvInd; a++)
+ fAverage += fFactors[a];
+ fAverage /= nAvInd;
+ }
+ if (fNewFactor < 1.0)
+ nWorseTotal++;
+ else
+ nBetterTotal++;
+
+ char buf[300];
+ sprintf(buf,
+ "Former: %ld New: %ld It got %s (factor %f) (av. last %d Former/New is %f, "
+ "WorseTotal: %d, BetterTotal: %d)",
+ nTimeFormer, nTimeNew, fNewFactor < 1.0 ? "WORSE" : "BETTER",
+ fNewFactor < 1.0 ? 1.0 / fNewFactor : fNewFactor, nAvInd, fAverage, nWorseTotal,
+ nBetterTotal);
+ SAL_INFO("drawinglayer.processor2d", buf);
+ }
+}
+#endif
+}
+
+// support for rendering Bitmap and BitmapEx contents
+namespace drawinglayer
+{
+// static global VDev buffer for VclProcessor2D/VclPixelProcessor2D
+VDevBuffer& getVDevBuffer()
+{
+ // secure global instance with Vcl's safe destroyer of external (seen by
+ // library base) stuff, the remembered VDevs need to be deleted before
+ // Vcl's deinit
+ static vcl::DeleteOnDeinit<VDevBuffer> aVDevBuffer{};
+ return *aVDevBuffer.get();
+}
+
+impBufferDevice::impBufferDevice(OutputDevice& rOutDev, const basegfx::B2DRange& rRange)
+ : mrOutDev(rOutDev)
+ , mpContent(nullptr)
+ , mpAlpha(nullptr)
+{
+ basegfx::B2DRange aRangePixel(rRange);
+ aRangePixel.transform(mrOutDev.GetViewTransformation());
+ maDestPixel = tools::Rectangle(floor(aRangePixel.getMinX()), floor(aRangePixel.getMinY()),
+ ceil(aRangePixel.getMaxX()), ceil(aRangePixel.getMaxY()));
+ maDestPixel.Intersection(tools::Rectangle{ Point{}, mrOutDev.GetOutputSizePixel() });
+
+ if (!isVisible())
+ return;
+
+ mpContent = getVDevBuffer().alloc(mrOutDev, maDestPixel.GetSize());
+
+ // #i93485# assert when copying from window to VDev is used
+ SAL_WARN_IF(
+ mrOutDev.GetOutDevType() == OUTDEV_WINDOW, "drawinglayer",
+ "impBufferDevice render helper: Copying from Window to VDev, this should be avoided (!)");
+
+ // initialize buffer by blitting content of source to prepare for
+ // transparence/ copying back
+ const bool bWasEnabledSrc(mrOutDev.IsMapModeEnabled());
+ mrOutDev.EnableMapMode(false);
+ mpContent->DrawOutDev(Point(), maDestPixel.GetSize(), maDestPixel.TopLeft(),
+ maDestPixel.GetSize(), mrOutDev);
+ mrOutDev.EnableMapMode(bWasEnabledSrc);
+
+ MapMode aNewMapMode(mrOutDev.GetMapMode());
+
+ const Point aLogicTopLeft(mrOutDev.PixelToLogic(maDestPixel.TopLeft()));
+ aNewMapMode.SetOrigin(Point(-aLogicTopLeft.X(), -aLogicTopLeft.Y()));
+
+ mpContent->SetMapMode(aNewMapMode);
+
+ // copy AA flag for new target
+ mpContent->SetAntialiasing(mrOutDev.GetAntialiasing());
+
+ // copy RasterOp (e.g. may be RasterOp::Xor on destination)
+ mpContent->SetRasterOp(mrOutDev.GetRasterOp());
+}
+
+impBufferDevice::~impBufferDevice()
+{
+ if (mpContent)
+ {
+ getVDevBuffer().free(*mpContent);
+ }
+
+ if (mpAlpha)
+ {
+ getVDevBuffer().free(*mpAlpha);
+ }
+}
+
+void impBufferDevice::paint(double fTrans)
+{
+ if (!isVisible())
+ return;
+
+ const Point aEmptyPoint;
+ const Size aSizePixel(maDestPixel.GetSize());
+ const bool bWasEnabledDst(mrOutDev.IsMapModeEnabled());
+
+ mrOutDev.EnableMapMode(false);
+ mpContent->EnableMapMode(false);
+
+#ifdef DBG_UTIL
+ // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
+ static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
+ static const OUString sDumpPath(OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
+
+ if (!sDumpPath.isEmpty() && bDoSaveForVisualControl)
+ {
+ SvFileStream aNew(sDumpPath + "content.bmp", StreamMode::WRITE | StreamMode::TRUNC);
+ Bitmap aContent(mpContent->GetBitmap(aEmptyPoint, aSizePixel));
+ WriteDIB(aContent, aNew, false, true);
+ }
+#endif
+
+ // during painting the buffer, disable evtl. set RasterOp (may be RasterOp::Xor)
+ const RasterOp aOrigRasterOp(mrOutDev.GetRasterOp());
+ mrOutDev.SetRasterOp(RasterOp::OverPaint);
+
+ if (mpAlpha)
+ {
+ mpAlpha->EnableMapMode(false);
+ AlphaMask aAlphaMask(mpAlpha->GetBitmap(aEmptyPoint, aSizePixel));
+ aAlphaMask.Invert(); // convert transparency to alpha
+
+#ifdef DBG_UTIL
+ if (!sDumpPath.isEmpty() && bDoSaveForVisualControl)
+ {
+ SvFileStream aNew(sDumpPath + "transparence.bmp",
+ StreamMode::WRITE | StreamMode::TRUNC);
+ WriteDIB(aAlphaMask.GetBitmap(), aNew, false, true);
+ }
+#endif
+
+ Bitmap aContent(mpContent->GetBitmap(aEmptyPoint, aSizePixel));
+ mrOutDev.DrawBitmapEx(maDestPixel.TopLeft(), BitmapEx(aContent, aAlphaMask));
+ }
+ else if (0.0 != fTrans)
+ {
+ const Bitmap aContent(mpContent->GetBitmap(aEmptyPoint, aSizePixel));
+
+#ifdef SPEED_COMPARE
+ static bool bCompareFormerAndNewTimings(true);
+
+ if (bCompareFormerAndNewTimings)
+ {
+ doSpeedCompare(fTrans, aContent, maDestPixel, mrOutDev);
+ }
+ else
+#endif
+ // Note: this extra scope is needed due to 'clang plugin indentation'. It complains
+ // that lines 494 and (now) 539 are 'statement mis-aligned compared to neighbours'.
+ // That is true if SPEED_COMPARE is not defined. Not nice, but have to fix this.
+ {
+ // For the case we have a unified transparency value there is a former
+ // and new method to paint that which can be used. To decide on measurements,
+ // I added 'doSpeedCompare' above which can be activated by defining
+ // SPEED_COMPARE at the top of this file.
+ // I added the used Testdoc: blurplay3.odg as
+ // https://bugs.documentfoundation.org/attachment.cgi?id=182463
+ // I did measure on
+ //
+ // Linux Dbg:
+ // Former: 21 New: 32 It got WORSE (factor 1.523810) (av. last 500 Former/New is 0.968533, WorseTotal: 515, BetterTotal: 934)
+ //
+ // Linux Pro:
+ // Former: 27 New: 44 It got WORSE (factor 1.629630) (av. last 500 Former/New is 0.923256, WorseTotal: 433, BetterTotal: 337)
+ //
+ // Win Dbg:
+ // Former: 21 New: 78 It got WORSE (factor 3.714286) (av. last 500 Former/New is 1.007176, WorseTotal: 85, BetterTotal: 1428)
+ //
+ // Win Pro:
+ // Former: 3 New: 4 It got WORSE (factor 1.333333) (av. last 500 Former/New is 1.054167, WorseTotal: 143, BetterTotal: 3909)
+ //
+ // Note: I am aware that the Dbg are of limited usefulness, but include them here
+ // for reference.
+ //
+ // The important part is "av. last 500 Former/New is %ld" which describes the averaged factor from Former/New
+ // over the last 500 measurements. When < 1.0 Former is better (Linux), > 1.0 (Win) New is better. Since the
+ // factor on Win is still close to 1.0 what means we lose nearly nothing and Linux Former is better, I will
+ // use Former for now.
+ //
+ // To easily allow to change this (maybe system-dependent) I add a static switch here,
+ // also for eventually experimenting (hint: can be changed in the debugger).
+ static bool bUseNew(false);
+
+ if (bUseNew)
+ {
+ // New method using DrawTransformedBitmapEx & fTrans directly
+ mrOutDev.DrawTransformedBitmapEx(basegfx::utils::createScaleTranslateB2DHomMatrix(
+ aSizePixel.Width(), aSizePixel.Height(),
+ maDestPixel.TopLeft().X(),
+ maDestPixel.TopLeft().Y()),
+ BitmapEx(aContent), 1 - fTrans);
+ }
+ else
+ {
+ // "Former" method using a temporary AlphaMask & DrawBitmapEx
+ sal_uInt8 nMaskValue(static_cast<sal_uInt8>(basegfx::fround(fTrans * 255.0)));
+ const AlphaMask aAlphaMask(aSizePixel, &nMaskValue);
+ mrOutDev.DrawBitmapEx(maDestPixel.TopLeft(), BitmapEx(aContent, aAlphaMask));
+ }
+ }
+ }
+ else
+ {
+ mrOutDev.DrawOutDev(maDestPixel.TopLeft(), aSizePixel, aEmptyPoint, aSizePixel, *mpContent);
+ }
+
+ mrOutDev.SetRasterOp(aOrigRasterOp);
+ mrOutDev.EnableMapMode(bWasEnabledDst);
+}
+
+VirtualDevice& impBufferDevice::getContent()
+{
+ SAL_WARN_IF(!mpContent, "drawinglayer",
+ "impBufferDevice: No content, check isVisible() before accessing (!)");
+ return *mpContent;
+}
+
+VirtualDevice& impBufferDevice::getTransparence()
+{
+ SAL_WARN_IF(!mpContent, "drawinglayer",
+ "impBufferDevice: No content, check isVisible() before accessing (!)");
+ if (!mpAlpha)
+ {
+ mpAlpha = getVDevBuffer().alloc(mrOutDev, maDestPixel.GetSize());
+ mpAlpha->SetMapMode(mpContent->GetMapMode());
+
+ // copy AA flag for new target; masking needs to be smooth
+ mpAlpha->SetAntialiasing(mpContent->GetAntialiasing());
+ }
+
+ return *mpAlpha;
+}
+} // end of namespace drawinglayer
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */