cache results of Skia's drawTransformedBitmap() (tdf#132438)

E.g. scrolling in Writer with a huge image inserted requires scaling
down on every paint, so cache the result if it's expensive.

Change-Id: I9db040eab47e0e9d7fd416ad064caf0301d346fb
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/94118
Tested-by: Jenkins
Reviewed-by: Luboš Luňák <l.lunak@collabora.com>
This commit is contained in:
Luboš Luňák
2020-05-14 11:57:30 +02:00
parent 13a834c26d
commit fc0bff85f3
4 changed files with 201 additions and 21 deletions
+1
View File
@@ -44,6 +44,7 @@ SAL_DISABLESKIA=1 - force disabled Skia
SAL_ENABLESKIA=1 - enable Skia, unless blacklisted (and if the VCL backend supports Skia)
SAL_FORCESKIA=1 - force using Skia, even if blacklisted
SAL_SKIA=raster|vulkan - select Skia's drawing method, by default Vulkan is used
SAL_DISABLE_SKIA_CACHE=1 - disable caching of complex images
OpenGL,Skia
-----------
+21
View File
@@ -52,6 +52,11 @@ VCL_DLLPUBLIC sk_sp<SkImage> createSkImage(const SkBitmap& bitmap);
VCL_DLLPUBLIC void
prepareSkia(std::unique_ptr<sk_app::WindowContext> (*createVulkanWindowContext)(bool));
// Shared cache of images.
void addCachedImage(const OString& key, sk_sp<SkImage> image);
sk_sp<SkImage> findCachedImage(const OString& key);
void removeCachedImage(sk_sp<SkImage> image);
#ifdef DBG_UTIL
void prefillSurface(sk_sp<SkSurface>& surface);
VCL_DLLPUBLIC void dump(const SkBitmap& bitmap, const char* file);
@@ -104,6 +109,22 @@ inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, t
return stream;
}
template <typename charT, typename traits>
inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& stream,
const SkImage& image)
{
// G - on GPU
return stream << static_cast<const void*>(&image) << " " << Size(image.width(), image.height())
<< "/" << (SkColorTypeBytesPerPixel(image.imageInfo().colorType()) * 8)
<< (image.isTextureBacked() ? "G" : "");
}
template <typename charT, typename traits>
inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& stream,
const sk_sp<SkImage>& image)
{
return stream << *image;
}
#endif // INCLUDED_VCL_INC_SKIA_UTILS_H
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
+79
View File
@@ -31,6 +31,7 @@ bool isVCLSkiaEnabled() { return false; }
#include <config_folders.h>
#include <osl/file.hxx>
#include <tools/stream.hxx>
#include <list>
#include <SkCanvas.h>
#include <SkPaint.h>
@@ -439,10 +440,88 @@ sk_sp<SkImage> createSkImage(const SkBitmap& bitmap)
return image;
}
namespace
{
// Image cache, for saving results of complex operations such as drawTransformedBitmap().
struct ImageCacheItem
{
OString key;
sk_sp<SkImage> image;
int size; // cost of the item
};
} //namespace
// LRU cache, last item is the least recently used. Hopefully there won't be that many items
// to require a hash/map. Using o3tl::lru_cache would be simpler, but it doesn't support
// calculating cost of each item.
static std::list<ImageCacheItem>* imageCache = nullptr;
static int imageCacheSize = 0; // sum of all ImageCacheItem.size
void addCachedImage(const OString& key, sk_sp<SkImage> image)
{
static bool disabled = getenv("SAL_DISABLE_SKIA_CACHE") != nullptr;
if (disabled)
return;
if (imageCache == nullptr)
imageCache = new std::list<ImageCacheItem>;
int size = image->width() * image->height()
* SkColorTypeBytesPerPixel(image->imageInfo().colorType());
imageCache->push_front({ key, image, size });
imageCacheSize += size;
SAL_INFO("vcl.skia.trace", "addcachedimage " << image << " :" << size << "/" << imageCacheSize);
const int MAX_CACHE_SIZE = 4 * 1000 * 1000 * 4; // 4x 1000px 32bpp images, 16MiB
while (imageCacheSize > MAX_CACHE_SIZE)
{
assert(!imageCache->empty());
imageCacheSize -= imageCache->back().size;
SAL_INFO("vcl.skia.trace",
"least used removal " << image << ":" << imageCache->back().size);
imageCache->pop_back();
}
}
sk_sp<SkImage> findCachedImage(const OString& key)
{
if (imageCache != nullptr)
{
for (auto it = imageCache->begin(); it != imageCache->end(); ++it)
{
if (it->key == key)
{
sk_sp<SkImage> ret = it->image;
SAL_INFO("vcl.skia.trace", "findcachedimage " << it->image);
imageCache->splice(imageCache->begin(), *imageCache, it);
return ret;
}
}
}
return nullptr;
}
void removeCachedImage(sk_sp<SkImage> image)
{
if (imageCache == nullptr)
return;
for (auto it = imageCache->begin(); it != imageCache->end();)
{
if (it->image == image)
{
imageCacheSize -= it->size;
assert(imageCacheSize >= 0);
it = imageCache->erase(it);
}
else
++it;
}
}
void cleanup()
{
delete sharedGrContext;
sharedGrContext = nullptr;
delete imageCache;
imageCache = nullptr;
imageCacheSize = 0;
}
// Skia should not be used from VCL backends that do not actually support it, as there will be setup missing.
+100 -21
View File
@@ -1293,6 +1293,90 @@ void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp<SkIma
postDraw();
}
// Create SkImage from a bitmap and possibly an alpha mask (the usual VCL one-minus-alpha),
// with the given target size. Result will be possibly cached, unless disabled.
static sk_sp<SkImage> mergeBitmaps(const SkiaSalBitmap& bitmap, const SkiaSalBitmap* alphaBitmap,
const Size targetSize, bool blockCaching)
{
sk_sp<SkImage> image;
OString key;
if (targetSize == bitmap.GetSize())
blockCaching = true; // probably not much point in caching if no scaling is involved
if (targetSize.Width() > bitmap.GetSize().Width()
|| targetSize.Height() > bitmap.GetSize().Height())
blockCaching = true; // caching enlarging is probably wasteful and not worth it
if (bitmap.GetSize().Width() < 100 && bitmap.GetSize().Height() < 100)
blockCaching = true; // image too small to be worth caching
if (!blockCaching)
{
OStringBuffer keyBuf;
keyBuf.append("0x")
.append(reinterpret_cast<sal_IntPtr>(&bitmap), 16)
.append("_0x")
.append(reinterpret_cast<sal_IntPtr>(alphaBitmap), 16)
.append("_")
.append(static_cast<sal_Int64>(bitmap.GetSkImage()->uniqueID()));
if (alphaBitmap)
keyBuf.append("_").append(
static_cast<sal_Int64>(alphaBitmap->GetAlphaSkImage()->uniqueID()));
key = keyBuf.makeStringAndClear();
image = SkiaHelper::findCachedImage(key);
if (image)
return image;
}
// Combine bitmap + alpha bitmap into one temporary bitmap with alpha.
// If scaling is needed, first apply the alpha, then scale, otherwise the scaling might affect the alpha values.
if (alphaBitmap && targetSize != bitmap.GetSize())
{
sk_sp<SkSurface> mergedSurface = SkiaHelper::createSkSurface(bitmap.GetSize());
if (!mergedSurface)
return nullptr;
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha
mergedSurface->getCanvas()->drawImage(bitmap.GetSkImage(), 0, 0, &paint);
paint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha
mergedSurface->getCanvas()->drawImage(alphaBitmap->GetAlphaSkImage(), 0, 0, &paint);
sk_sp<SkSurface> scaledSurface = SkiaHelper::createSkSurface(targetSize);
if (!scaledSurface)
return nullptr;
paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha
paint.setFilterQuality(kHigh_SkFilterQuality);
scaledSurface->getCanvas()->drawImageRect(
mergedSurface->makeImageSnapshot(),
SkRect::MakeXYWH(0, 0, bitmap.GetSize().Width(), bitmap.GetSize().Height()),
SkRect::MakeXYWH(0, 0, targetSize.Width(), targetSize.Height()), &paint);
image = scaledSurface->makeImageSnapshot();
}
else // No alpha or no scaling, scale directly.
{
sk_sp<SkSurface> tmpSurface = SkiaHelper::createSkSurface(targetSize);
if (!tmpSurface)
return nullptr;
SkCanvas* canvas = tmpSurface->getCanvas();
SkAutoCanvasRestore autoRestore(canvas, true);
SkPaint paint;
if (targetSize != bitmap.GetSize())
{
SkMatrix matrix;
matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / bitmap.GetSize().Width());
matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / bitmap.GetSize().Height());
canvas->concat(matrix);
paint.setFilterQuality(kHigh_SkFilterQuality);
}
paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha
canvas->drawImage(bitmap.GetSkImage(), 0, 0, &paint);
if (alphaBitmap != nullptr)
{
paint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha
canvas->drawImage(alphaBitmap->GetAlphaSkImage(), 0, 0, &paint);
}
image = tmpSurface->makeImageSnapshot();
}
if (!blockCaching)
SkiaHelper::addCachedImage(key, image);
return image;
}
bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull,
const basegfx::B2DPoint& rX,
const basegfx::B2DPoint& rY,
@@ -1305,44 +1389,39 @@ bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull,
const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
const SkiaSalBitmap* pSkiaAlphaBitmap = static_cast<const SkiaSalBitmap*>(pAlphaBitmap);
sk_sp<SkSurface> tmpSurface = SkiaHelper::createSkSurface(rSourceBitmap.GetSize());
if (!tmpSurface)
return false;
// Combine bitmap + alpha bitmap into one temporary bitmap with alpha
SkCanvas* aCanvas = tmpSurface->getCanvas();
SkPaint aPaint;
aPaint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha
aCanvas->drawImage(rSkiaBitmap.GetSkImage(), 0, 0, &aPaint);
if (pSkiaAlphaBitmap != nullptr)
{
aPaint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha
aCanvas->drawImage(pSkiaAlphaBitmap->GetAlphaSkImage(), 0, 0, &aPaint);
}
// setup the image transformation
// using the rNull, rX, rY points as destinations for the (0,0), (0,Width), (Height,0) source points
// using the rNull, rX, rY points as destinations for the (0,0), (Width,0), (0,Height) source points
const basegfx::B2DVector aXRel = rX - rNull;
const basegfx::B2DVector aYRel = rY - rNull;
const Size aSize = rSourceBitmap.GetSize();
preDraw();
SAL_INFO("vcl.skia.trace", "drawtransformedbitmap(" << this << "): " << aSize << " " << rNull
<< ":" << rX << ":" << rY);
// TODO: How to cache properly skewed images?
bool blockCaching = (aXRel.getY() != 0 || aYRel.getX() != 0);
const Size imageSize(aXRel.getX(), aYRel.getY());
sk_sp<SkImage> imageToDraw
= mergeBitmaps(rSkiaBitmap, pSkiaAlphaBitmap, imageSize, blockCaching);
if (!imageToDraw)
return false;
SkMatrix aMatrix;
aMatrix.set(SkMatrix::kMScaleX, aXRel.getX() / aSize.Width());
aMatrix.set(SkMatrix::kMScaleX, aXRel.getX() / imageToDraw->width());
aMatrix.set(SkMatrix::kMSkewY, aXRel.getY() / aSize.Width());
aMatrix.set(SkMatrix::kMSkewX, aYRel.getX() / aSize.Height());
aMatrix.set(SkMatrix::kMScaleY, aYRel.getY() / aSize.Height());
aMatrix.set(SkMatrix::kMScaleY, aYRel.getY() / imageToDraw->height());
aMatrix.set(SkMatrix::kMTransX, rNull.getX());
aMatrix.set(SkMatrix::kMTransY, rNull.getY());
preDraw();
SAL_INFO("vcl.skia.trace",
"drawtransformedbitmap(" << this << "): " << rNull << ":" << rX << ":" << rY);
{
SkAutoCanvasRestore autoRestore(getDrawCanvas(), true);
getDrawCanvas()->concat(aMatrix);
SkPaint paint;
paint.setFilterQuality(kHigh_SkFilterQuality);
getDrawCanvas()->drawImage(tmpSurface->makeImageSnapshot(), 0, 0, &paint);
getDrawCanvas()->drawImage(imageToDraw, 0, 0, &paint);
}
assert(!mXorMode);
postDraw();