| /* | |
| * Copyright 2014 Google Inc. | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkBitmap.h" | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkColorSpace.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkImageInfo.h" | |
| #include "include/core/SkMatrix.h" | |
| #include "include/core/SkPicture.h" // IWYU pragma: keep | |
| #include "include/core/SkPictureRecorder.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkSamplingOptions.h" | |
| #include "include/core/SkSize.h" | |
| #include "include/core/SkSurface.h" | |
| #include "include/core/SkTypes.h" | |
| #include "include/private/chromium/SkDiscardableMemory.h" | |
| #include "src/core/SkBitmapCache.h" | |
| #include "src/core/SkCachedData.h" | |
| #include "src/core/SkMipmap.h" | |
| #include "src/core/SkResourceCache.h" | |
| #include "src/image/SkImage_Base.h" | |
| #include "src/lazy/SkDiscardableMemoryPool.h" | |
| #include "tests/Test.h" | |
| #include <array> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <initializer_list> | |
| #include <memory> | |
| //////////////////////////////////////////////////////////////////////////////////////// | |
| enum LockedState { | |
| kNotLocked, | |
| kLocked, | |
| }; | |
| enum CachedState { | |
| kNotInCache, | |
| kInCache, | |
| }; | |
| static void check_data(skiatest::Reporter* reporter, const SkCachedData* data, | |
| int refcnt, CachedState cacheState, LockedState lockedState) { | |
| REPORTER_ASSERT(reporter, data->testing_only_getRefCnt() == refcnt); | |
| REPORTER_ASSERT(reporter, data->testing_only_isInCache() == (kInCache == cacheState)); | |
| bool isLocked = (data->data() != nullptr); | |
| REPORTER_ASSERT(reporter, isLocked == (lockedState == kLocked)); | |
| } | |
| static void test_mipmapcache(skiatest::Reporter* reporter, SkResourceCache* cache) { | |
| cache->purgeAll(); | |
| SkBitmap src; | |
| src.allocN32Pixels(5, 5); | |
| src.setImmutable(); | |
| sk_sp<SkImage> img = src.asImage(); | |
| const auto desc = SkBitmapCacheDesc::Make(img.get()); | |
| const SkMipmap* mipmap = SkMipmapCache::FindAndRef(desc, cache); | |
| REPORTER_ASSERT(reporter, nullptr == mipmap); | |
| mipmap = SkMipmapCache::AddAndRef(as_IB(img.get()), cache); | |
| REPORTER_ASSERT(reporter, mipmap); | |
| { | |
| const SkMipmap* mm = SkMipmapCache::FindAndRef(desc, cache); | |
| REPORTER_ASSERT(reporter, mm); | |
| REPORTER_ASSERT(reporter, mm == mipmap); | |
| mm->unref(); | |
| } | |
| check_data(reporter, mipmap, 2, kInCache, kLocked); | |
| mipmap->unref(); | |
| // tricky, since technically after this I'm no longer an owner, but since the cache is | |
| // local, I know it won't get purged behind my back | |
| check_data(reporter, mipmap, 1, kInCache, kNotLocked); | |
| // find us again | |
| mipmap = SkMipmapCache::FindAndRef(desc, cache); | |
| check_data(reporter, mipmap, 2, kInCache, kLocked); | |
| cache->purgeAll(); | |
| check_data(reporter, mipmap, 1, kNotInCache, kLocked); | |
| mipmap->unref(); | |
| } | |
| static void test_mipmap_notify(skiatest::Reporter* reporter, SkResourceCache* cache) { | |
| const int N = 3; | |
| SkBitmap src[N]; | |
| sk_sp<SkImage> img[N]; | |
| SkBitmapCacheDesc desc[N]; | |
| for (int i = 0; i < N; ++i) { | |
| src[i].allocN32Pixels(5, 5); | |
| src[i].setImmutable(); | |
| img[i] = src[i].asImage(); | |
| SkMipmapCache::AddAndRef(as_IB(img[i].get()), cache)->unref(); | |
| desc[i] = SkBitmapCacheDesc::Make(img[i].get()); | |
| } | |
| for (int i = 0; i < N; ++i) { | |
| const SkMipmap* mipmap = SkMipmapCache::FindAndRef(desc[i], cache); | |
| // We're always using a local cache, so we know we won't be purged by other threads | |
| REPORTER_ASSERT(reporter, mipmap); | |
| SkSafeUnref(mipmap); | |
| img[i].reset(); // delete the image, which *should not* remove us from the cache | |
| mipmap = SkMipmapCache::FindAndRef(desc[i], cache); | |
| REPORTER_ASSERT(reporter, mipmap); | |
| SkSafeUnref(mipmap); | |
| src[i].reset(); // delete the underlying pixelref, which *should* remove us from the cache | |
| mipmap = SkMipmapCache::FindAndRef(desc[i], cache); | |
| REPORTER_ASSERT(reporter, !mipmap); | |
| } | |
| } | |
| static SkDiscardableMemoryPool* gPool = nullptr; | |
| static int gFactoryCalls = 0; | |
| static SkDiscardableMemory* pool_factory(size_t bytes) { | |
| SkASSERT(gPool); | |
| gFactoryCalls++; | |
| return gPool->create(bytes); | |
| } | |
| static void testBitmapCache_discarded_bitmap(skiatest::Reporter* reporter, SkResourceCache* cache, | |
| SkResourceCache::DiscardableFactory factory) { | |
| test_mipmapcache(reporter, cache); | |
| test_mipmap_notify(reporter, cache); | |
| } | |
| DEF_TEST(BitmapCache_discarded_bitmap, reporter) { | |
| const size_t byteLimit = 100 * 1024; | |
| { | |
| SkResourceCache cache(byteLimit); | |
| testBitmapCache_discarded_bitmap(reporter, &cache, nullptr); | |
| } | |
| { | |
| sk_sp<SkDiscardableMemoryPool> pool(SkDiscardableMemoryPool::Make(byteLimit)); | |
| gPool = pool.get(); | |
| SkResourceCache::DiscardableFactory factory = pool_factory; | |
| SkResourceCache cache(factory); | |
| testBitmapCache_discarded_bitmap(reporter, &cache, factory); | |
| } | |
| REPORTER_ASSERT(reporter, gFactoryCalls > 0); | |
| } | |
| static void test_discarded_image(skiatest::Reporter* reporter, const SkMatrix& transform, | |
| sk_sp<SkImage> (*buildImage)()) { | |
| auto surface(SkSurfaces::Raster(SkImageInfo::MakeN32Premul(10, 10))); | |
| SkCanvas* canvas = surface->getCanvas(); | |
| // SkBitmapCache is global, so other threads could be evicting our bitmaps. Loop a few times | |
| // to mitigate this risk. | |
| const unsigned kRepeatCount = 42; | |
| for (unsigned i = 0; i < kRepeatCount; ++i) { | |
| SkAutoCanvasRestore acr(canvas, true); | |
| sk_sp<SkImage> image(buildImage()); | |
| // draw the image (with a transform, to tickle different code paths) to ensure | |
| // any associated resources get cached | |
| canvas->concat(transform); | |
| // always use high quality to ensure caching when scaled | |
| canvas->drawImage(image, 0, 0, SkSamplingOptions({1.0f/3, 1.0f/3})); | |
| const auto desc = SkBitmapCacheDesc::Make(image.get()); | |
| // delete the image | |
| image.reset(nullptr); | |
| // all resources should have been purged | |
| SkBitmap result; | |
| REPORTER_ASSERT(reporter, !SkBitmapCache::Find(desc, &result)); | |
| } | |
| } | |
| // Verify that associated bitmap cache entries are purged on SkImage destruction. | |
| DEF_TEST(BitmapCache_discarded_image, reporter) { | |
| // Cache entries associated with SkImages fall into two categories: | |
| // | |
| // 1) generated image bitmaps (managed by the image cacherator) | |
| // 2) scaled/resampled bitmaps (cached when HQ filters are used) | |
| // | |
| // To exercise the first cache type, we use generated/picture-backed SkImages. | |
| // To exercise the latter, we draw scaled bitmap images using HQ filters. | |
| const SkMatrix xforms[] = { | |
| SkMatrix::Scale(1, 1), | |
| SkMatrix::Scale(1.7f, 0.5f), | |
| }; | |
| for (size_t i = 0; i < std::size(xforms); ++i) { | |
| test_discarded_image(reporter, xforms[i], []() { | |
| auto surface(SkSurfaces::Raster(SkImageInfo::MakeN32Premul(10, 10))); | |
| surface->getCanvas()->clear(SK_ColorCYAN); | |
| return surface->makeImageSnapshot(); | |
| }); | |
| test_discarded_image(reporter, xforms[i], []() { | |
| SkPictureRecorder recorder; | |
| SkCanvas* canvas = recorder.beginRecording(10, 10); | |
| canvas->clear(SK_ColorCYAN); | |
| return SkImages::DeferredFromPicture(recorder.finishRecordingAsPicture(), | |
| SkISize::Make(10, 10), | |
| nullptr, | |
| nullptr, | |
| SkImages::BitDepth::kU8, | |
| SkColorSpace::MakeSRGB()); | |
| }); | |
| } | |
| } | |
| /////////////////////////////////////////////////////////////////////////////////////////////////// | |
| static void* gTestNamespace; | |
| struct TestKey : SkResourceCache::Key { | |
| int32_t fData; | |
| TestKey(int sharedID, int32_t data) : fData(data) { | |
| this->init(&gTestNamespace, sharedID, sizeof(fData)); | |
| } | |
| }; | |
| struct TestRec : SkResourceCache::Rec { | |
| enum { | |
| kDidInstall = 1 << 0, | |
| }; | |
| TestKey fKey; | |
| int* fFlags; | |
| bool fCanBePurged; | |
| TestRec(int sharedID, int32_t data, int* flagPtr) : fKey(sharedID, data), fFlags(flagPtr) { | |
| fCanBePurged = false; | |
| } | |
| const Key& getKey() const override { return fKey; } | |
| size_t bytesUsed() const override { return 1024; /* just need a value */ } | |
| bool canBePurged() override { return fCanBePurged; } | |
| void postAddInstall(void*) override { | |
| *fFlags |= kDidInstall; | |
| } | |
| const char* getCategory() const override { return "test-category"; } | |
| }; | |
| static void test_duplicate_add(SkResourceCache* cache, skiatest::Reporter* reporter, | |
| bool purgable) { | |
| int sharedID = 1; | |
| int data = 0; | |
| int flags0 = 0, flags1 = 0; | |
| auto rec0 = std::make_unique<TestRec>(sharedID, data, &flags0); | |
| auto rec1 = std::make_unique<TestRec>(sharedID, data, &flags1); | |
| SkASSERT(rec0->getKey() == rec1->getKey()); | |
| TestRec* r0 = rec0.get(); // save the bare-pointer since we will release rec0 | |
| r0->fCanBePurged = purgable; | |
| REPORTER_ASSERT(reporter, !(flags0 & TestRec::kDidInstall)); | |
| REPORTER_ASSERT(reporter, !(flags1 & TestRec::kDidInstall)); | |
| cache->add(rec0.release(), nullptr); | |
| REPORTER_ASSERT(reporter, flags0 & TestRec::kDidInstall); | |
| REPORTER_ASSERT(reporter, !(flags1 & TestRec::kDidInstall)); | |
| flags0 = 0; // reset the flag | |
| cache->add(rec1.release(), nullptr); | |
| if (purgable) { | |
| // we purged rec0, and did install rec1 | |
| REPORTER_ASSERT(reporter, !(flags0 & TestRec::kDidInstall)); | |
| REPORTER_ASSERT(reporter, flags1 & TestRec::kDidInstall); | |
| } else { | |
| // we re-used rec0 and did not install rec1 | |
| REPORTER_ASSERT(reporter, flags0 & TestRec::kDidInstall); | |
| REPORTER_ASSERT(reporter, !(flags1 & TestRec::kDidInstall)); | |
| r0->fCanBePurged = true; // so we can cleanup the cache | |
| } | |
| } | |
| /* | |
| * Test behavior when the same key is added more than once. | |
| */ | |
| DEF_TEST(ResourceCache_purge, reporter) { | |
| for (bool purgable : { false, true }) { | |
| { | |
| SkResourceCache cache(1024 * 1024); | |
| test_duplicate_add(&cache, reporter, purgable); | |
| } | |
| { | |
| SkResourceCache cache(SkDiscardableMemory::Create); | |
| test_duplicate_add(&cache, reporter, purgable); | |
| } | |
| } | |
| } |