diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /drawinglayer/source/processor2d/vclhelperbufferdevice.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drawinglayer/source/processor2d/vclhelperbufferdevice.cxx')
-rw-r--r-- | drawinglayer/source/processor2d/vclhelperbufferdevice.cxx | 593 |
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: */ |