diff options
-rw-r--r-- | vcl/inc/skia/gdiimpl.hxx | 4 | ||||
-rw-r--r-- | vcl/inc/skia/salbmp.hxx | 21 | ||||
-rw-r--r-- | vcl/inc/skia/utils.hxx | 8 | ||||
-rw-r--r-- | vcl/qa/cppunit/skia/skia.cxx | 37 | ||||
-rw-r--r-- | vcl/skia/gdiimpl.cxx | 141 | ||||
-rw-r--r-- | vcl/skia/salbmp.cxx | 49 |
6 files changed, 212 insertions, 48 deletions
diff --git a/vcl/inc/skia/gdiimpl.hxx b/vcl/inc/skia/gdiimpl.hxx index abfa89ca8bfa..9cf14f176185 100644 --- a/vcl/inc/skia/gdiimpl.hxx +++ b/vcl/inc/skia/gdiimpl.hxx @@ -284,6 +284,10 @@ protected: static void setCanvasClipRegion(SkCanvas* canvas, const vcl::Region& region); sk_sp<SkImage> mergeCacheBitmaps(const SkiaSalBitmap& bitmap, const SkiaSalBitmap* alphaBitmap, const Size& targetSize); + using DirectImage = SkiaHelper::DirectImage; + static OString makeCachedImageKey(const SkiaSalBitmap& bitmap, const SkiaSalBitmap* alphaBitmap, + const Size& targetSize, DirectImage bitmapType, + DirectImage alphaBitmapType); // Skia uses floating point coordinates, so when we use integer coordinates, sometimes // rounding results in off-by-one errors (down), especially when drawing using GPU, diff --git a/vcl/inc/skia/salbmp.hxx b/vcl/inc/skia/salbmp.hxx index aa8d245ce741..2959952a3442 100644 --- a/vcl/inc/skia/salbmp.hxx +++ b/vcl/inc/skia/salbmp.hxx @@ -70,17 +70,21 @@ public: // True if GetSkShader() should be preferred to GetSkImage() (or the Alpha variants). bool PreferSkShader() const; + // Direct image means direct access to the stored SkImage, without checking + // if its size is up to date. This should be used only in special cases with care. + using DirectImage = SkiaHelper::DirectImage; // Returns the contents as SkImage (possibly GPU-backed). - const sk_sp<SkImage>& GetSkImage() const; - sk_sp<SkShader> GetSkShader(const SkSamplingOptions& samplingOptions) const; - + const sk_sp<SkImage>& GetSkImage(DirectImage direct = DirectImage::No) const; + sk_sp<SkShader> GetSkShader(const SkSamplingOptions& samplingOptions, + DirectImage direct = DirectImage::No) const; // Returns the contents as alpha SkImage (possibly GPU-backed) - const sk_sp<SkImage>& GetAlphaSkImage() const; - sk_sp<SkShader> GetAlphaSkShader(const SkSamplingOptions& samplingOptions) const; + const sk_sp<SkImage>& GetAlphaSkImage(DirectImage direct = DirectImage::No) const; + sk_sp<SkShader> GetAlphaSkShader(const SkSamplingOptions& samplingOptions, + DirectImage direct = DirectImage::No) const; // Key for caching/hashing. - OString GetImageKey() const; - OString GetAlphaImageKey() const; + OString GetImageKey(DirectImage direct = DirectImage::No) const; + OString GetAlphaImageKey(DirectImage direct = DirectImage::No) const; // Returns true if it is known that this bitmap can be ignored if it's to be used // as an alpha bitmap. An optimization, not guaranteed to return true for all such cases. @@ -88,6 +92,9 @@ public: // Alpha type best suitable for the content. SkAlphaType alphaType() const; + // Tries to create direct GetAlphaSkImage() from direct GetSkImage(). + void TryDirectConvertToAlphaNoScaling(); + // Dump contents to a file for debugging. void dump(const char* file) const; diff --git a/vcl/inc/skia/utils.hxx b/vcl/inc/skia/utils.hxx index 0a17ee81bc4d..92df0325c870 100644 --- a/vcl/inc/skia/utils.hxx +++ b/vcl/inc/skia/utils.hxx @@ -73,6 +73,14 @@ VCL_DLLPUBLIC sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface, inline Size imageSize(const sk_sp<SkImage>& image) { return Size(image->width(), image->height()); } +// Whether to use GetSkImage() that checks for delayed scaling or whether to access +// the stored image directly without checks. +enum DirectImage +{ + Yes, + No +}; + // Do 'paint->setBlendMode(SkBlendMode::kDifference)' (workaround for buggy drivers). void setBlendModeDifference(SkPaint* paint); diff --git a/vcl/qa/cppunit/skia/skia.cxx b/vcl/qa/cppunit/skia/skia.cxx index 128829aecc18..f6920ccf1d8a 100644 --- a/vcl/qa/cppunit/skia/skia.cxx +++ b/vcl/qa/cppunit/skia/skia.cxx @@ -43,6 +43,7 @@ public: void testMatrixQuality(); void testDelayedScale(); void testDelayedScaleAlphaImage(); + void testDrawDelayedScaleImage(); void testTdf137329(); void testTdf140848(); void testTdf132367(); @@ -56,6 +57,7 @@ public: CPPUNIT_TEST(testMatrixQuality); CPPUNIT_TEST(testDelayedScale); CPPUNIT_TEST(testDelayedScaleAlphaImage); + CPPUNIT_TEST(testDrawDelayedScaleImage); CPPUNIT_TEST(testTdf137329); CPPUNIT_TEST(testTdf140848); CPPUNIT_TEST(testTdf132367); @@ -429,6 +431,41 @@ void SkiaTest::testDelayedScaleAlphaImage() CPPUNIT_ASSERT_EQUAL(Size(240, 240), bitmapCopy.GetSize()); } +void SkiaTest::testDrawDelayedScaleImage() +{ + if (!SkiaHelper::isVCLSkiaEnabled()) + return; + ScopedVclPtr<VirtualDevice> device = VclPtr<VirtualDevice>::Create(DeviceFormat::DEFAULT); + device->SetOutputSizePixel(Size(10, 10)); + device->SetBackground(Wallpaper(COL_WHITE)); + device->Erase(); + Bitmap bitmap(Size(10, 10), vcl::PixelFormat::N24_BPP); + bitmap.Erase(COL_RED); + // Set a pixel to create pixel data. + BitmapWriteAccess(bitmap).SetPixel(0, 0, COL_BLUE); + SkiaSalBitmap* skiaBitmap1 = dynamic_cast<SkiaSalBitmap*>(bitmap.ImplGetSalBitmap().get()); + // Force creating of image. + sk_sp<SkImage> image1 = skiaBitmap1->GetSkImage(); + CPPUNIT_ASSERT(skiaBitmap1->unittestHasImage()); + CPPUNIT_ASSERT(bitmap.Scale(Size(5, 5))); + // Make sure delayed scaling has not changed the image. + SkiaSalBitmap* skiaBitmap2 = dynamic_cast<SkiaSalBitmap*>(bitmap.ImplGetSalBitmap().get()); + CPPUNIT_ASSERT(skiaBitmap2->unittestHasImage()); + sk_sp<SkImage> image2 = skiaBitmap2->GetSkImage(SkiaHelper::DirectImage::Yes); + CPPUNIT_ASSERT_EQUAL(image1, image2); + CPPUNIT_ASSERT_EQUAL(Size(5, 5), bitmap.GetSizePixel()); + CPPUNIT_ASSERT_EQUAL(Size(10, 10), SkiaHelper::imageSize(image2)); + // Draw the bitmap scaled to size 10x10 and check that the 10x10 image was used (and kept), + // even though technically the bitmap is 5x5. + device->DrawBitmap(Point(0, 0), Size(10, 10), bitmap); + SkiaSalBitmap* skiaBitmap3 = dynamic_cast<SkiaSalBitmap*>(bitmap.ImplGetSalBitmap().get()); + CPPUNIT_ASSERT(skiaBitmap3->unittestHasImage()); + sk_sp<SkImage> image3 = skiaBitmap3->GetSkImage(SkiaHelper::DirectImage::Yes); + CPPUNIT_ASSERT_EQUAL(image1, image3); + CPPUNIT_ASSERT_EQUAL(Size(5, 5), bitmap.GetSizePixel()); + CPPUNIT_ASSERT_EQUAL(Size(10, 10), SkiaHelper::imageSize(image3)); +} + void SkiaTest::testTdf137329() { if (!SkiaHelper::isVCLSkiaEnabled()) diff --git a/vcl/skia/gdiimpl.cxx b/vcl/skia/gdiimpl.cxx index f532e48528a9..13925700f31d 100644 --- a/vcl/skia/gdiimpl.cxx +++ b/vcl/skia/gdiimpl.cxx @@ -1489,11 +1489,10 @@ std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(tools::Long nX, tools: sk_sp<SkImage> image = makeCheckedImageSnapshot( mSurface, scaleRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), mScaling)); std::shared_ptr<SkiaSalBitmap> bitmap = std::make_shared<SkiaSalBitmap>(image); - // TODO: If the surface is scaled for HiDPI, the bitmap needs to be scaled down, otherwise - // it would have incorrect size from the API point of view. This could lead to loss of quality - // if the bitmap is drawn to another scaled surface. Since the bitmap scaling is done only - // on-demand, this state should be detected when drawing the bitmap and the scaling - // should be ignored. + // If the surface is scaled for HiDPI, the bitmap needs to be scaled down, otherwise + // it would have incorrect size from the API point of view. The DirectImage::Yes handling + // in mergeCacheBitmaps() should access the original unscaled bitmap data to avoid + // pointless scaling back and forth. if (mScaling != 1) { if (!isUnitTestRunning()) @@ -1621,20 +1620,68 @@ sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitma const SkiaSalBitmap* alphaBitmap, const Size& targetSize) { - sk_sp<SkImage> image; + if (alphaBitmap) + assert(bitmap.GetSize() == alphaBitmap->GetSize()); + if (targetSize.IsEmpty()) - return image; + return {}; if (alphaBitmap && alphaBitmap->IsFullyOpaqueAsAlpha()) alphaBitmap = nullptr; // the alpha can be ignored if (bitmap.PreferSkShader() && (!alphaBitmap || alphaBitmap->PreferSkShader())) - return image; + return {}; + + // If the bitmap has SkImage that matches the required size, try to use it, even + // if it doesn't match bitmap.GetSize(). This can happen with delayed scaling. + // This will catch cases such as some code pre-scaling the bitmap, which would make GetSkImage() + // scale, changing GetImageKey() in the process so we'd have to re-cache, and then we'd need + // to scale again in this function. + bool bitmapReady = false; + bool alphaBitmapReady = false; + if (const sk_sp<SkImage>& image = bitmap.GetSkImage(DirectImage::Yes)) + { + assert(!bitmap.PreferSkShader()); + if (imageSize(image) == targetSize) + bitmapReady = true; + } + // If the image usable and there's no alpha, then it matches exactly what's wanted. + if (bitmapReady && !alphaBitmap) + return bitmap.GetSkImage(DirectImage::Yes); + if (alphaBitmap) + { + if (!alphaBitmap->GetAlphaSkImage(DirectImage::Yes) + && alphaBitmap->GetSkImage(DirectImage::Yes) + && imageSize(alphaBitmap->GetSkImage(DirectImage::Yes)) == targetSize) + { + // There's a usable non-alpha image, try to convert it to alpha. + assert(!alphaBitmap->PreferSkShader()); + const_cast<SkiaSalBitmap*>(alphaBitmap)->TryDirectConvertToAlphaNoScaling(); + } + if (const sk_sp<SkImage>& image = alphaBitmap->GetAlphaSkImage(DirectImage::Yes)) + { + assert(!alphaBitmap->PreferSkShader()); + if (imageSize(image) == targetSize) + alphaBitmapReady = true; + } + } + + if (bitmapReady && (!alphaBitmap || alphaBitmapReady)) + { + // Try to find a cached image based on the already existing images. + OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, DirectImage::Yes, + DirectImage::Yes); + if (sk_sp<SkImage> image = findCachedImage(key)) + { + assert(imageSize(image) == targetSize); + return image; + } + } // Probably not much point in caching of just doing a copy. if (alphaBitmap == nullptr && targetSize == bitmap.GetSize()) - return image; + return {}; // Image too small to be worth caching if not scaling. if (targetSize == bitmap.GetSize() && targetSize.Width() < 100 && targetSize.Height() < 100) - return image; + return {}; // GPU-accelerated drawing with SkShader should be fast enough to not need caching. if (isGPU()) { @@ -1643,7 +1690,7 @@ sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitma int reduceRatio = bitmap.GetSize().Width() * bitmap.GetSize().Height() / targetSize.Width() / targetSize.Height(); if (reduceRatio < 10) - return image; + return {}; } // In some cases (tdf#134237) the target size may be very large. In that case it's // better to rely on Skia to clip and draw only the necessary, rather than prepare @@ -1669,22 +1716,45 @@ sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitma << this << "): not caching, ratio:" << ratio << ", " << bitmap.GetSize() << "->" << targetSize << " in " << drawAreaSize); - return image; + return {}; } } // Do not cache the result if it would take most of the cache and thus get evicted soon. if (targetSize.Width() * targetSize.Height() * 4 > maxImageCacheSize() * 0.7) - return image; - OString key = OString::number(targetSize.Width()) + "x" + OString::number(targetSize.Height()) - + "_" + bitmap.GetImageKey(); - if (alphaBitmap) - key += "_" + alphaBitmap->GetAlphaImageKey(); - image = findCachedImage(key); - if (image) - { - assert(image->width() == targetSize.Width() && image->height() == targetSize.Height()); + return {}; + + // Use ready direct image if they are both available, now even the size doesn't matter + // (we'll scale as necessary and it's better to scale from the original). Require only + // that they are the same size, or that one prefers a shader or doesn't exist + // (i.e. avoid two images of different size). + bitmapReady = bitmap.GetSkImage(DirectImage::Yes) != nullptr; + alphaBitmapReady + = alphaBitmap ? alphaBitmap->GetAlphaSkImage(DirectImage::Yes) != nullptr : false; + if (bitmapReady && alphaBitmap && !alphaBitmapReady && !alphaBitmap->PreferSkShader()) + bitmapReady = false; + if (alphaBitmapReady && !bitmapReady && bitmap.PreferSkShader()) + alphaBitmapReady = false; + + DirectImage bitmapType = bitmapReady ? DirectImage::Yes : DirectImage::No; + DirectImage alphaBitmapType = alphaBitmapReady ? DirectImage::Yes : DirectImage::No; + + // Try to find a cached result, this time after possible delayed scaling. + OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, bitmapType, alphaBitmapType); + if (sk_sp<SkImage> image = findCachedImage(key)) + { + assert(imageSize(image) == targetSize); return image; } + + Size sourceSize; + if (bitmapReady) + sourceSize = imageSize(bitmap.GetSkImage(DirectImage::Yes)); + else if (alphaBitmapReady) + sourceSize = imageSize(alphaBitmap->GetAlphaSkImage(DirectImage::Yes)); + else + sourceSize = bitmap.GetSize(); + + // Generate a new result and cache it. sk_sp<SkSurface> tmpSurface = createSkSurface(targetSize, alphaBitmap ? kPremul_SkAlphaType : bitmap.alphaType()); if (!tmpSurface) @@ -1693,36 +1763,49 @@ sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitma SkAutoCanvasRestore autoRestore(canvas, true); SkPaint paint; SkSamplingOptions samplingOptions; - if (targetSize != bitmap.GetSize()) + if (targetSize != sourceSize) { SkMatrix matrix; - matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / bitmap.GetSize().Width()); - matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / bitmap.GetSize().Height()); + matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / sourceSize.Width()); + matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / sourceSize.Height()); canvas->concat(matrix); samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix, 1); } if (alphaBitmap != nullptr) { canvas->clear(SK_ColorTRANSPARENT); - paint.setShader(SkShaders::Blend(SkBlendMode::kDstOut, bitmap.GetSkShader(samplingOptions), - alphaBitmap->GetAlphaSkShader(samplingOptions))); + paint.setShader( + SkShaders::Blend(SkBlendMode::kDstOut, bitmap.GetSkShader(samplingOptions, bitmapType), + alphaBitmap->GetAlphaSkShader(samplingOptions, alphaBitmapType))); canvas->drawPaint(paint); } else if (bitmap.PreferSkShader()) { - paint.setShader(bitmap.GetSkShader(samplingOptions)); + paint.setShader(bitmap.GetSkShader(samplingOptions, bitmapType)); canvas->drawPaint(paint); } else - canvas->drawImage(bitmap.GetSkImage(), 0, 0, samplingOptions, &paint); + canvas->drawImage(bitmap.GetSkImage(bitmapType), 0, 0, samplingOptions, &paint); if (isGPU()) SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" << this << "): caching GPU downscaling:" << bitmap.GetSize() << "->" << targetSize); - image = makeCheckedImageSnapshot(tmpSurface); + sk_sp<SkImage> image = makeCheckedImageSnapshot(tmpSurface); addCachedImage(key, image); return image; } +OString SkiaSalGraphicsImpl::makeCachedImageKey(const SkiaSalBitmap& bitmap, + const SkiaSalBitmap* alphaBitmap, + const Size& targetSize, DirectImage bitmapType, + DirectImage alphaBitmapType) +{ + OString key = OString::number(targetSize.Width()) + "x" + OString::number(targetSize.Height()) + + "_" + bitmap.GetImageKey(bitmapType); + if (alphaBitmap) + key += "_" + alphaBitmap->GetAlphaImageKey(alphaBitmapType); + return key; +} + bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap, const SalBitmap& rAlphaBitmap) { diff --git a/vcl/skia/salbmp.cxx b/vcl/skia/salbmp.cxx index dd96e3ecd094..8e1938d15c50 100644 --- a/vcl/skia/salbmp.cxx +++ b/vcl/skia/salbmp.cxx @@ -736,11 +736,13 @@ bool SkiaSalBitmap::ConserveMemory() const && (mBitCount > 8 || (mBitCount == 8 && mPalette.IsGreyPalette8Bit())); } -const sk_sp<SkImage>& SkiaSalBitmap::GetSkImage() const +const sk_sp<SkImage>& SkiaSalBitmap::GetSkImage(DirectImage direct) const { #ifdef DBG_UTIL assert(mWriteAccessCount == 0); #endif + if (direct == DirectImage::Yes) + return mImage; if (mEraseColorSet) { if (mImage) @@ -812,11 +814,13 @@ const sk_sp<SkImage>& SkiaSalBitmap::GetSkImage() const return mImage; } -const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage() const +const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage(DirectImage direct) const { #ifdef DBG_UTIL assert(mWriteAccessCount == 0); #endif + if (direct == DirectImage::Yes) + return mAlphaImage; if (mEraseColorSet) { if (mAlphaImage) @@ -836,8 +840,8 @@ const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage() const } if (mAlphaImage) { - assert(imageSize(mAlphaImage) == mSize); // data has already been scaled if needed - return mAlphaImage; + if (imageSize(mAlphaImage) == mSize) + return mAlphaImage; } if (mImage) { @@ -967,22 +971,41 @@ const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage() const return mAlphaImage; } +void SkiaSalBitmap::TryDirectConvertToAlphaNoScaling() +{ + // This is a bit of a hack. Because of the VCL alpha hack where alpha is stored + // separately, we often convert mImage to mAlphaImage to represent the alpha + // channel. If code finds out that there is mImage but no mAlphaImage, + // this will create it from it, without checking for delayed scaling (i.e. + // it is "direct"). + assert(mImage); + assert(!mAlphaImage); + // Set wanted size, trigger conversion. + Size savedSize = mSize; + mSize = imageSize(mImage); + GetAlphaSkImage(); + assert(mAlphaImage); + mSize = savedSize; +} + // If the bitmap is to be erased, SkShader with the color set is more efficient // than creating an image filled with the color. bool SkiaSalBitmap::PreferSkShader() const { return mEraseColorSet; } -sk_sp<SkShader> SkiaSalBitmap::GetSkShader(const SkSamplingOptions& samplingOptions) const +sk_sp<SkShader> SkiaSalBitmap::GetSkShader(const SkSamplingOptions& samplingOptions, + DirectImage direct) const { if (mEraseColorSet) return SkShaders::Color(toSkColor(mEraseColor)); - return GetSkImage()->makeShader(samplingOptions); + return GetSkImage(direct)->makeShader(samplingOptions); } -sk_sp<SkShader> SkiaSalBitmap::GetAlphaSkShader(const SkSamplingOptions& samplingOptions) const +sk_sp<SkShader> SkiaSalBitmap::GetAlphaSkShader(const SkSamplingOptions& samplingOptions, + DirectImage direct) const { if (mEraseColorSet) return SkShaders::Color(fromEraseColorToAlphaImageColor(mEraseColor)); - return GetAlphaSkImage()->makeShader(samplingOptions); + return GetAlphaSkImage(direct)->makeShader(samplingOptions); } bool SkiaSalBitmap::IsFullyOpaqueAsAlpha() const @@ -1314,7 +1337,7 @@ void SkiaSalBitmap::ResetPendingScaling() mAlphaImage.reset(); } -OString SkiaSalBitmap::GetImageKey() const +OString SkiaSalBitmap::GetImageKey(DirectImage direct) const { if (mEraseColorSet) { @@ -1324,10 +1347,11 @@ OString SkiaSalBitmap::GetImageKey() const << static_cast<int>(mEraseColor.GetAlpha()); return OString::Concat("E") + ss.str().c_str(); } - return OString::Concat("I") + OString::number(GetSkImage()->uniqueID()); + assert(direct == DirectImage::No || mImage); + return OString::Concat("I") + OString::number(GetSkImage(direct)->uniqueID()); } -OString SkiaSalBitmap::GetAlphaImageKey() const +OString SkiaSalBitmap::GetAlphaImageKey(DirectImage direct) const { if (mEraseColorSet) { @@ -1336,7 +1360,8 @@ OString SkiaSalBitmap::GetAlphaImageKey() const << static_cast<int>(255 - SkColorGetA(fromEraseColorToAlphaImageColor(mEraseColor))); return OString::Concat("E") + ss.str().c_str(); } - return OString::Concat("I") + OString::number(GetAlphaSkImage()->uniqueID()); + assert(direct == DirectImage::No || mAlphaImage); + return OString::Concat("I") + OString::number(GetAlphaSkImage(direct)->uniqueID()); } void SkiaSalBitmap::dump(const char* file) const |