/* * Copyright 2020 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/SkCanvas.h" #include "include/core/SkDeferredDisplayListRecorder.h" #include "include/core/SkSurfaceCharacterization.h" #include "include/private/SkMalloc.h" #include "include/utils/SkRandom.h" #include "src/core/SkCanvasPriv.h" #include "src/core/SkMessageBus.h" #include "src/gpu/GrDefaultGeoProcFactory.h" #include "src/gpu/GrDirectContextPriv.h" #include "src/gpu/GrGpu.h" #include "src/gpu/GrMemoryPool.h" #include "src/gpu/GrOpFlushState.h" #include "src/gpu/GrProxyProvider.h" #include "src/gpu/GrRecordingContextPriv.h" #include "src/gpu/GrStyle.h" #include "src/gpu/GrSurfaceDrawContext.h" #include "src/gpu/GrThreadSafeCache.h" #include "tests/Test.h" #include "tests/TestUtils.h" #include "tools/gpu/ProxyUtils.h" #include static constexpr int kImageWH = 32; static constexpr auto kImageOrigin = kBottomLeft_GrSurfaceOrigin; static constexpr int kNoID = -1; static SkImageInfo default_ii(int wh) { return SkImageInfo::Make(wh, wh, kRGBA_8888_SkColorType, kPremul_SkAlphaType); } static std::unique_ptr new_RTC(GrRecordingContext* rContext, int wh) { return GrSurfaceDrawContext::Make(rContext, GrColorType::kRGBA_8888, nullptr, SkBackingFit::kExact, {wh, wh}, 1, GrMipMapped::kNo, GrProtected::kNo, kImageOrigin, SkBudgeted::kYes); } static void create_view_key(GrUniqueKey* key, int wh, int id) { static const GrUniqueKey::Domain kViewDomain = GrUniqueKey::GenerateDomain(); GrUniqueKey::Builder builder(key, kViewDomain, 1); builder[0] = wh; builder.finish(); if (id != kNoID) { key->setCustomData(SkData::MakeWithCopy(&id, sizeof(id))); } } static void create_vert_key(GrUniqueKey* key, int wh, int id) { static const GrUniqueKey::Domain kVertDomain = GrUniqueKey::GenerateDomain(); GrUniqueKey::Builder builder(key, kVertDomain, 1); builder[0] = wh; builder.finish(); if (id != kNoID) { key->setCustomData(SkData::MakeWithCopy(&id, sizeof(id))); } } static bool default_is_newer_better(SkData* incumbent, SkData* challenger) { return false; } // When testing views we create a bitmap that covers the entire screen and has an inset blue rect // atop a field of white. // When testing verts we clear the background to white and simply draw an inset blur rect. static SkBitmap create_bitmap(int wh) { SkBitmap bitmap; bitmap.allocPixels(default_ii(wh)); SkCanvas tmp(bitmap); tmp.clear(SK_ColorWHITE); SkPaint blue; blue.setColor(SK_ColorBLUE); blue.setAntiAlias(false); tmp.drawRect({10, 10, wh-10.0f, wh-10.0f}, blue); bitmap.setImmutable(); return bitmap; } class GrThreadSafeVertexTestOp; class TestHelper { public: struct Stats { int fCacheHits = 0; int fCacheMisses = 0; int fNumSWCreations = 0; int fNumLazyCreations = 0; int fNumHWCreations = 0; }; TestHelper(GrDirectContext* dContext, GrThreadSafeCache::IsNewerBetter isNewerBetter = default_is_newer_better) : fDContext(dContext) , fIsNewerBetter(isNewerBetter) { fDst = SkSurface::MakeRenderTarget(dContext, SkBudgeted::kNo, default_ii(kImageWH)); SkAssertResult(fDst); SkSurfaceCharacterization characterization; SkAssertResult(fDst->characterize(&characterization)); fRecorder1 = std::make_unique(characterization); this->ddlCanvas1()->clear(SkColors::kWhite); fRecorder2 = std::make_unique(characterization); this->ddlCanvas2()->clear(SkColors::kWhite); fDst->getCanvas()->clear(SkColors::kWhite); } ~TestHelper() { fDContext->flush(); fDContext->submit(true); } Stats* stats() { return &fStats; } int numCacheEntries() const { return this->threadSafeCache()->numEntries(); } GrDirectContext* dContext() { return fDContext; } SkCanvas* liveCanvas() { return fDst ? fDst->getCanvas() : nullptr; } SkCanvas* ddlCanvas1() { return fRecorder1 ? fRecorder1->getCanvas() : nullptr; } sk_sp snap1() { if (fRecorder1) { sk_sp tmp = fRecorder1->detach(); fRecorder1 = nullptr; return tmp; } return nullptr; } SkCanvas* ddlCanvas2() { return fRecorder2 ? fRecorder2->getCanvas() : nullptr; } sk_sp snap2() { if (fRecorder2) { sk_sp tmp = fRecorder2->detach(); fRecorder2 = nullptr; return tmp; } return nullptr; } GrThreadSafeCache* threadSafeCache() { return fDContext->priv().threadSafeCache(); } const GrThreadSafeCache* threadSafeCache() const { return fDContext->priv().threadSafeCache(); } typedef void (TestHelper::*addAccessFP)(SkCanvas*, int wh, int id, bool failLookUp, bool failFillingIn); typedef bool (TestHelper::*checkFP)(SkCanvas*, int wh, int expectedHits, int expectedMisses, int expectedNumRefs, int expectedID); // Add a draw on 'canvas' that will introduce a ref on the 'wh' view void addViewAccess(SkCanvas* canvas, int wh, int id = kNoID, bool failLookup = false, bool failFillingIn = false) { auto rContext = canvas->recordingContext(); auto view = AccessCachedView(rContext, this->threadSafeCache(), wh, failLookup, failFillingIn, id, &fStats); SkASSERT(view); auto sdc = SkCanvasPriv::TopDeviceSurfaceDrawContext(canvas); sdc->drawTexture(nullptr, view, kPremul_SkAlphaType, GrSamplerState::Filter::kNearest, GrSamplerState::MipmapMode::kNone, SkBlendMode::kSrcOver, {1.0f, 1.0f, 1.0f, 1.0f}, SkRect::MakeWH(wh, wh), SkRect::MakeWH(wh, wh), GrAA::kNo, GrQuadAAFlags::kNone, SkCanvas::kFast_SrcRectConstraint, SkMatrix::I(), nullptr); } // Besides checking that the number of refs and cache hits and misses are as expected, this // method also validates that the unique key doesn't appear in any of the other caches. bool checkView(SkCanvas* canvas, int wh, int expectedHits, int expectedMisses, int expectedNumRefs, int expectedID) { if (fStats.fCacheHits != expectedHits || fStats.fCacheMisses != expectedMisses) { SkDebugf("Hits E: %d A: %d --- Misses E: %d A: %d\n", expectedHits, fStats.fCacheHits, expectedMisses, fStats.fCacheMisses); return false; } GrUniqueKey key; create_view_key(&key, wh, kNoID); auto threadSafeCache = this->threadSafeCache(); auto [view, xtraData] = threadSafeCache->findWithData(key); if (!view.proxy()) { return false; } if (expectedID < 0) { if (xtraData) { return false; } } else { if (!xtraData) { return false; } const int* cachedID = static_cast(xtraData->data()); if (*cachedID != expectedID) { return false; } } if (!view.proxy()->refCntGreaterThan(expectedNumRefs+1) || // +1 for 'view's ref view.proxy()->refCntGreaterThan(expectedNumRefs+2)) { return false; } if (canvas) { GrRecordingContext* rContext = canvas->recordingContext(); GrProxyProvider* recordingProxyProvider = rContext->priv().proxyProvider(); sk_sp result = recordingProxyProvider->findProxyByUniqueKey(key); if (result) { // views in this cache should never appear in the recorder's cache return false; } } { GrProxyProvider* directProxyProvider = fDContext->priv().proxyProvider(); sk_sp result = directProxyProvider->findProxyByUniqueKey(key); if (result) { // views in this cache should never appear in the main proxy cache return false; } } { auto resourceProvider = fDContext->priv().resourceProvider(); sk_sp surf = resourceProvider->findByUniqueKey(key); if (surf) { // the textures backing the views in this cache should never be discoverable in the // resource cache return false; } } return true; } void addVertAccess(SkCanvas* canvas, int wh, int id, bool failLookup, bool failFillingIn, GrThreadSafeVertexTestOp** createdOp); // Add a draw on 'canvas' that will introduce a ref on a 'wh' vertex data void addVertAccess(SkCanvas* canvas, int wh, int id = kNoID, bool failLookup = false, bool failFillingIn = false) { this->addVertAccess(canvas, wh, id, failLookup, failFillingIn, nullptr); } bool checkVert(SkCanvas* canvas, int wh, int expectedHits, int expectedMisses, int expectedNumRefs, int expectedID) { if (fStats.fCacheHits != expectedHits || fStats.fCacheMisses != expectedMisses) { SkDebugf("Hits E: %d A: %d --- Misses E: %d A: %d\n", expectedHits, fStats.fCacheHits, expectedMisses, fStats.fCacheMisses); return false; } GrUniqueKey key; create_vert_key(&key, wh, kNoID); auto threadSafeCache = this->threadSafeCache(); auto [vertData, xtraData] = threadSafeCache->findVertsWithData(key); if (!vertData) { return false; } if (expectedID < 0) { if (xtraData) { return false; } } else { if (!xtraData) { return false; } const int* cachedID = static_cast(xtraData->data()); if (*cachedID != expectedID) { return false; } } if (!vertData->refCntGreaterThan(expectedNumRefs+1) || // +1 for 'vertData's ref vertData->refCntGreaterThan(expectedNumRefs+2)) { return false; } { auto resourceProvider = fDContext->priv().resourceProvider(); sk_sp buffer = resourceProvider->findByUniqueKey(key); if (buffer) { // the buffer holding the vertex data in this cache should never be discoverable // in the resource cache return false; } } return true; } bool checkImage(skiatest::Reporter* reporter, sk_sp s) { SkBitmap actual; actual.allocPixels(default_ii(kImageWH)); if (!s->readPixels(actual, 0, 0)) { return false; } SkBitmap expected = create_bitmap(kImageWH); const float tols[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; auto error = std::function( [reporter](int x, int y, const float diffs[4]) { SkASSERT(x >= 0 && y >= 0); ERRORF(reporter, "mismatch at %d, %d (%f, %f, %f %f)", x, y, diffs[0], diffs[1], diffs[2], diffs[3]); }); return ComparePixels(expected.pixmap(), actual.pixmap(), tols, error); } bool checkImage(skiatest::Reporter* reporter) { return this->checkImage(reporter, fDst); } bool checkImage(skiatest::Reporter* reporter, sk_sp ddl) { sk_sp tmp = SkSurface::MakeRenderTarget(fDContext, SkBudgeted::kNo, default_ii(kImageWH)); if (!tmp) { return false; } if (!tmp->draw(std::move(ddl))) { return false; } return this->checkImage(reporter, std::move(tmp)); } size_t gpuSize(int wh) const { GrBackendFormat format = fDContext->defaultBackendFormat(kRGBA_8888_SkColorType, GrRenderable::kNo); return GrSurface::ComputeSize(format, {wh, wh}, /*colorSamplesPerPixel=*/1, GrMipMapped::kNo, /*binSize=*/false); } private: static GrSurfaceProxyView AccessCachedView(GrRecordingContext*, GrThreadSafeCache*, int wh, bool failLookup, bool failFillingIn, int id, Stats*); static GrSurfaceProxyView CreateViewOnCpu(GrRecordingContext*, int wh, Stats*); static bool FillInViewOnGpu(GrDirectContext*, int wh, Stats*, const GrSurfaceProxyView& lazyView, sk_sp); Stats fStats; GrDirectContext* fDContext = nullptr; GrThreadSafeCache::IsNewerBetter fIsNewerBetter; sk_sp fDst; std::unique_ptr fRecorder1; std::unique_ptr fRecorder2; }; class GrThreadSafeVertexTestOp : public GrDrawOp { public: DEFINE_OP_CLASS_ID static GrOp::Owner Make(GrRecordingContext* rContext, TestHelper::Stats* stats, int wh, int id, bool failLookup, bool failFillingIn, GrThreadSafeCache::IsNewerBetter isNewerBetter) { return GrOp::Make( rContext, rContext, stats, wh, id, failLookup, failFillingIn, isNewerBetter); } const GrThreadSafeCache::VertexData* vertexData() const { return fVertexData.get(); } private: friend class GrOp; // for ctor GrThreadSafeVertexTestOp(GrRecordingContext* rContext, TestHelper::Stats* stats, int wh, int id, bool failLookup, bool failFillingIn, GrThreadSafeCache::IsNewerBetter isNewerBetter) : INHERITED(ClassID()) , fStats(stats) , fWH(wh) , fID(id) , fFailFillingIn(failFillingIn) , fIsNewerBetter(isNewerBetter) { this->setBounds(SkRect::MakeIWH(fWH, fWH), HasAABloat::kNo, IsHairline::kNo); // Normally we wouldn't add a ref to the vertex data at this point. However, it is // needed in this unit test to get the ref counts on the uniquely keyed resources // to be as expected. this->findOrCreateVertices(rContext, failLookup, fFailFillingIn); } const char* name() const override { return "GrThreadSafeVertexTestOp"; } FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; } GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*, bool hasMixedSampledCoverage, GrClampType) override { return GrProcessorSet::EmptySetAnalysis(); } GrProgramInfo* createProgramInfo(const GrCaps* caps, SkArenaAlloc* arena, const GrSurfaceProxyView& writeView, GrAppliedClip&& appliedClip, const GrXferProcessor::DstProxyView& dstProxyView, GrXferBarrierFlags renderPassXferBarriers, GrLoadOp colorLoadOp) const { using namespace GrDefaultGeoProcFactory; Color color({ 0.0f, 0.0f, 1.0f, 1.0f }); auto gp = MakeForDeviceSpace(arena, color, Coverage::kSolid_Type, LocalCoords::kUnused_Type, SkMatrix::I()); return sk_gpu_test::CreateProgramInfo(caps, arena, writeView, std::move(appliedClip), dstProxyView, gp, SkBlendMode::kSrcOver, GrPrimitiveType::kTriangleStrip, renderPassXferBarriers, colorLoadOp); } GrProgramInfo* createProgramInfo(GrOpFlushState* flushState) const { return this->createProgramInfo(&flushState->caps(), flushState->allocator(), flushState->writeView(), flushState->detachAppliedClip(), flushState->dstProxyView(), flushState->renderPassBarriers(), flushState->colorLoadOp()); } void findOrCreateVertices(GrRecordingContext* rContext, bool failLookup, bool failFillingIn) { if (!fVertexData) { auto threadSafeViewCache = rContext->priv().threadSafeCache(); if (rContext->asDirectContext()) { // The vertex variant doesn't have a correlate to lazyProxies but increment this // here to make the unit tests happy. ++fStats->fNumLazyCreations; } GrUniqueKey key; create_vert_key(&key, fWH, fID); // We can "fail the lookup" to simulate a threaded race condition auto [cachedVerts, data] = threadSafeViewCache->findVertsWithData(key); if (cachedVerts && !failLookup) { fVertexData = cachedVerts; ++fStats->fCacheHits; return; } ++fStats->fCacheMisses; if (!rContext->asDirectContext()) { ++fStats->fNumSWCreations; } constexpr size_t kVertSize = sizeof(SkPoint); SkPoint* verts = static_cast(sk_malloc_throw(4 * kVertSize)); verts[0].set(10.0f, 10.0f); verts[1].set(fWH-10.0f, 10.0f); verts[2].set(10.0f, fWH-10.0f); verts[3].set(fWH-10.0f, fWH-10.0f); fVertexData = GrThreadSafeCache::MakeVertexData(verts, 4, kVertSize); auto [tmpV, tmpD] = threadSafeViewCache->addVertsWithData(key, fVertexData, fIsNewerBetter); if (tmpV != fVertexData) { // Someone beat us to creating the vertex data. Use that version. fVertexData = tmpV; } } if (auto dContext = rContext->asDirectContext(); dContext && !fVertexData->gpuBuffer()) { auto rp = dContext->priv().resourceProvider(); if (!failFillingIn) { ++fStats->fNumHWCreations; sk_sp tmp = rp->createBuffer(fVertexData->size(), GrGpuBufferType::kVertex, kStatic_GrAccessPattern, fVertexData->vertices()); fVertexData->setGpuBuffer(std::move(tmp)); } } } void onPrePrepare(GrRecordingContext* rContext, const GrSurfaceProxyView& writeView, GrAppliedClip* clip, const GrXferProcessor::DstProxyView& dstProxyView, GrXferBarrierFlags renderPassXferBarriers, GrLoadOp colorLoadOp) override { SkArenaAlloc* arena = rContext->priv().recordTimeAllocator(); // This is equivalent to a GrOpFlushState::detachAppliedClip GrAppliedClip appliedClip = clip ? std::move(*clip) : GrAppliedClip::Disabled(); fProgramInfo = this->createProgramInfo(rContext->priv().caps(), arena, writeView, std::move(appliedClip), dstProxyView, renderPassXferBarriers, colorLoadOp); rContext->priv().recordProgramInfo(fProgramInfo); // This is now a noop (bc it is always called in the ctor) but is where we would normally // create the vertices. this->findOrCreateVertices(rContext, false, fFailFillingIn); } void onPrepare(GrOpFlushState* flushState) override { auto dContext = flushState->gpu()->getContext(); // This call site is not a noop bc this op could've been created on with DDL context // and, therefore, could be lacking a gpu-side buffer this->findOrCreateVertices(dContext, false, fFailFillingIn); } void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override { if (!fVertexData || !fVertexData->gpuBuffer()) { return; } if (!fProgramInfo) { fProgramInfo = this->createProgramInfo(flushState); } flushState->bindPipeline(*fProgramInfo, SkRect::MakeIWH(fWH, fWH)); flushState->bindBuffers(nullptr, nullptr, fVertexData->refGpuBuffer()); flushState->draw(4, 0); } TestHelper::Stats* fStats; int fWH; int fID; bool fFailFillingIn; GrThreadSafeCache::IsNewerBetter fIsNewerBetter; sk_sp fVertexData; GrProgramInfo* fProgramInfo = nullptr; using INHERITED = GrDrawOp; }; void TestHelper::addVertAccess(SkCanvas* canvas, int wh, int id, bool failLookup, bool failFillingIn, GrThreadSafeVertexTestOp** createdOp) { auto rContext = canvas->recordingContext(); auto sdc = SkCanvasPriv::TopDeviceSurfaceDrawContext(canvas); GrOp::Owner op = GrThreadSafeVertexTestOp::Make(rContext, &fStats, wh, id, failLookup, failFillingIn, fIsNewerBetter); if (createdOp) { *createdOp = (GrThreadSafeVertexTestOp*) op.get(); } sdc->addDrawOp(std::move(op)); } GrSurfaceProxyView TestHelper::CreateViewOnCpu(GrRecordingContext* rContext, int wh, Stats* stats) { GrProxyProvider* proxyProvider = rContext->priv().proxyProvider(); sk_sp proxy = proxyProvider->createProxyFromBitmap(create_bitmap(wh), GrMipmapped::kNo, SkBackingFit::kExact, SkBudgeted::kYes); if (!proxy) { return {}; } GrSwizzle swizzle = rContext->priv().caps()->getReadSwizzle(proxy->backendFormat(), GrColorType::kRGBA_8888); ++stats->fNumSWCreations; return {std::move(proxy), kImageOrigin, swizzle}; } bool TestHelper::FillInViewOnGpu(GrDirectContext* dContext, int wh, Stats* stats, const GrSurfaceProxyView& lazyView, sk_sp trampoline) { std::unique_ptr rtc = new_RTC(dContext, wh); GrPaint paint; paint.setColor4f({0.0f, 0.0f, 1.0f, 1.0f}); rtc->clear(SkPMColor4f{1.0f, 1.0f, 1.0f, 1.0f}); rtc->drawRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(), { 10, 10, wh-10.0f, wh-10.0f }, &GrStyle::SimpleFill()); ++stats->fNumHWCreations; auto view = rtc->readSurfaceView(); SkASSERT(view.swizzle() == lazyView.swizzle()); SkASSERT(view.origin() == lazyView.origin()); trampoline->fProxy = view.asTextureProxyRef(); return true; } GrSurfaceProxyView TestHelper::AccessCachedView(GrRecordingContext* rContext, GrThreadSafeCache* threadSafeCache, int wh, bool failLookup, bool failFillingIn, int id, Stats* stats) { GrUniqueKey key; create_view_key(&key, wh, id); if (GrDirectContext* dContext = rContext->asDirectContext()) { // The gpu thread gets priority over the recording threads. If the gpu thread is first, // it crams a lazy proxy into the cache and then fills it in later. auto [lazyView, trampoline] = GrThreadSafeCache::CreateLazyView( dContext, GrColorType::kRGBA_8888, {wh, wh}, kImageOrigin, SkBackingFit::kExact); ++stats->fNumLazyCreations; auto [view, data] = threadSafeCache->findOrAddWithData(key, lazyView); if (view != lazyView) { ++stats->fCacheHits; return view; } else if (id != kNoID) { // Make sure, in this case, that the customData stuck SkASSERT(data); SkDEBUGCODE(const int* cachedID = static_cast(data->data());) SkASSERT(*cachedID == id); } ++stats->fCacheMisses; if (failFillingIn) { // Simulate something going horribly wrong at flush-time so no GrTexture is // available to fulfill the lazy proxy. return view; } if (!FillInViewOnGpu(dContext, wh, stats, lazyView, std::move(trampoline))) { // In this case something has gone disastrously wrong so set up to drop the draw // that needed this resource and reduce future pollution of the cache. threadSafeCache->remove(key); return {}; } return view; } else { GrSurfaceProxyView view; // We can "fail the lookup" to simulate a threaded race condition if (view = threadSafeCache->find(key); !failLookup && view) { ++stats->fCacheHits; return view; } ++stats->fCacheMisses; view = CreateViewOnCpu(rContext, wh, stats); SkASSERT(view); auto [newView, data] = threadSafeCache->addWithData(key, view); if (view == newView && id != kNoID) { // Make sure, in this case, that the customData stuck SkASSERT(data); SkDEBUGCODE(const int* cachedID = static_cast(data->data());) SkASSERT(*cachedID == id); } return newView; } } // Case 1: ensure two DDL recorders share the view/vertexData static void test_1(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 1, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1)); (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, 2, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, /*id*/ 1)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1); helper.checkImage(reporter, helper.snap1()); helper.checkImage(reporter, helper.snap2()); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache1View, reporter, ctxInfo) { test_1(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache1Verts, reporter, ctxInfo) { test_1(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 2: ensure that, if the direct context version wins, its result is reused by the // DDL recorders static void test_2(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, 1, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1)); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 2, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, /*id*/ 1)); (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, 3, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH, /*hits*/ 2, /*misses*/ 1, /*refs*/ 3, /*id*/ 1)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0); helper.checkImage(reporter); helper.checkImage(reporter, helper.snap1()); helper.checkImage(reporter, helper.snap2()); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache2View, reporter, ctxInfo) { test_2(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache2Verts, reporter, ctxInfo) { test_2(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 3: ensure that, if the cpu-version wins, its result is reused by the direct context static void test_3(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 1, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1)); (helper.*addAccess)(helper.liveCanvas(), kImageWH, 2, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, /*id*/ 1)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1); helper.checkImage(reporter); helper.checkImage(reporter, helper.snap1()); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache3View, reporter, ctxInfo) { test_3(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache3Verts, reporter, ctxInfo) { test_3(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 4: ensure that, if two DDL recorders get in a race, they still end up sharing a single // view/vertexData static void test_4(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 1, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1)); static const bool kFailLookup = true; (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, 2, kFailLookup, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 2, /*id*/ 1)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 2); helper.checkImage(reporter, helper.snap1()); helper.checkImage(reporter, helper.snap2()); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4View, reporter, ctxInfo) { test_4(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4Verts, reporter, ctxInfo) { test_4(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 4.5: check that, if a live rendering and a DDL recording get into a race, the live // rendering takes precedence. static void test_4_5(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, 1, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0); static const bool kFailLookup = true; (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 2, kFailLookup, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 2, /*id*/ 1)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1); helper.checkImage(reporter); helper.checkImage(reporter, helper.snap1()); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_5View, reporter, ctxInfo) { test_4_5(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_5Verts, reporter, ctxInfo) { test_4_5(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 4.75: check that, if a live rendering fails to generate the content needed to instantiate // its lazy proxy, life goes on static void test_4_75(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); static const bool kFailFillingIn = true; (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, kFailFillingIn); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0); dContext->flush(); dContext->submit(true); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 0, kNoID)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_75View, reporter, ctxInfo) { test_4_75(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_75Verts, reporter, ctxInfo) { test_4_75(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 5: ensure that expanding the map works (esp. wrt custom data) static void test_5(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); auto threadSafeCache = helper.threadSafeCache(); int size = 16; (helper.*addAccess)(helper.ddlCanvas1(), size, /*id*/ size, false, false); size_t initialSize = threadSafeCache->approxBytesUsedForHash(); while (initialSize == threadSafeCache->approxBytesUsedForHash()) { size *= 2; (helper.*addAccess)(helper.ddlCanvas1(), size, /*id*/ size, false, false); } for (int i = 16; i <= size; i *= 2) { REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), /*wh*/ i, /*hits*/ 0, /*misses*/ threadSafeCache->numEntries(), /*refs*/ 1, /*id*/ i)); } } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache5View, reporter, ctxInfo) { test_5(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache5Verts, reporter, ctxInfo) { test_5(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 6: Check on dropping refs. In particular, that the cache has its own ref to keep // the backing resource alive and locked. static void test_6(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false); sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); ddl1 = nullptr; REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 1, kNoID)); ddl2 = nullptr; REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 0, kNoID)); // The cache still has its ref REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 0, kNoID)); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache6View, reporter, ctxInfo) { test_6(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache6Verts, reporter, ctxInfo) { test_6(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 7: Check that invoking dropAllRefs and dropUniqueRefs directly works as expected; i.e., // dropAllRefs removes everything while dropUniqueRefs is more measured. static void test_7(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas2(), 2*kImageWH, kNoID, false, false); sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, (helper.*check)(nullptr, 2*kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); helper.threadSafeCache()->dropUniqueRefs(nullptr); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); ddl1 = nullptr; helper.threadSafeCache()->dropUniqueRefs(nullptr); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, (helper.*check)(nullptr, 2*kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID)); helper.threadSafeCache()->dropAllRefs(); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0); ddl2 = nullptr; } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache7View, reporter, ctxInfo) { test_7(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache7Verts, reporter, ctxInfo) { test_7(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 8: This checks that GrContext::abandonContext works as expected wrt the thread // safe cache. This simulates the case where we have one DDL that has finished // recording but one still recording when the abandonContext fires. static void test_8(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID)); (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH, /*hits*/ 2, /*misses*/ 1, /*refs*/ 3, kNoID)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0); dContext->abandonContext(); // This should exercise dropAllRefs sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0); ddl1 = nullptr; ddl2 = nullptr; } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache8View, reporter, ctxInfo) { test_8(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache8Verts, reporter, ctxInfo) { test_8(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 9: This checks that GrContext::releaseResourcesAndAbandonContext works as expected wrt // the thread safe cache. This simulates the case where we have one DDL that has finished // recording but one still recording when the releaseResourcesAndAbandonContext fires. static void test_9(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID)); (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH, /*hits*/ 2, /*misses*/ 1, /*refs*/ 3, kNoID)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1); REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0); dContext->releaseResourcesAndAbandonContext(); // This should hit dropAllRefs sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0); ddl1 = nullptr; ddl2 = nullptr; } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache9View, reporter, ctxInfo) { test_9(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache9Verts, reporter, ctxInfo) { test_9(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 10: This checks that the GrContext::purgeUnlockedResources(size_t) variant works as // expected wrt the thread safe cache. It, in particular, tests out the MRU behavior // of the shared cache. static void test_10(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { if (GrBackendApi::kOpenGL != dContext->backend()) { // The lower-level backends have too much going on for the following simple purging // test to work return; } TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID)); (helper.*addAccess)(helper.liveCanvas(), 2*kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 1, /*misses*/ 2, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas2(), 2*kImageWH, kNoID, false, false); sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), 2*kImageWH, /*hits*/ 2, /*misses*/ 2, /*refs*/ 2, kNoID)); dContext->flush(); dContext->submit(true); // This should clear out everything but the textures locked in the thread-safe cache dContext->purgeUnlockedResources(false); ddl1 = nullptr; ddl2 = nullptr; REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 2, /*misses*/ 2, /*refs*/ 0, kNoID)); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 2, /*misses*/ 2, /*refs*/ 0, kNoID)); // Regardless of which image is MRU, this should force the other out size_t desiredBytes = helper.gpuSize(2*kImageWH) + helper.gpuSize(kImageWH)/2; auto cache = dContext->priv().getResourceCache(); size_t currentBytes = cache->getResourceBytes(); SkASSERT(currentBytes >= desiredBytes); size_t amountToPurge = currentBytes - desiredBytes; // The 2*kImageWH texture should be MRU. dContext->purgeUnlockedResources(amountToPurge, true); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 2, /*misses*/ 2, /*refs*/ 0, kNoID)); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache10View, reporter, ctxInfo) { test_10(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } // To enable test_10 with verts would require a bit more work, namely: // have a different # of verts based on size // also pass in a gpuSize function to 'test_10' //DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache10Verts, reporter, ctxInfo) { // test_10(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); //} // Case 11: This checks that scratch-only variant of GrContext::purgeUnlockedResources works as // expected wrt the thread safe cache. In particular, that when 'scratchResourcesOnly' // is true, the call has no effect on the cache. static void test_11(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.liveCanvas(), 2*kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID)); dContext->flush(); dContext->submit(true); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 0, kNoID)); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 0, kNoID)); // This shouldn't remove anything from the cache dContext->purgeUnlockedResources(/* scratchResourcesOnly */ true); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); dContext->purgeUnlockedResources(/* scratchResourcesOnly */ false); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache11View, reporter, ctxInfo) { test_11(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache11Verts, reporter, ctxInfo) { test_11(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 12: Test out purges caused by resetting the cache budget to 0. Note that, due to // the how the cache operates (i.e., not directly driven by ref/unrefs) there // needs to be an explicit kick to purge the cache. static void test_12(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID)); (helper.*addAccess)(helper.liveCanvas(), 2*kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 1, /*misses*/ 2, /*refs*/ 1, kNoID)); dContext->flush(); dContext->submit(true); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH, /*hits*/ 1, /*misses*/ 2, /*refs*/ 1, kNoID)); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 1, /*misses*/ 2, /*refs*/ 0, kNoID)); dContext->setResourceCacheLimit(0); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); ddl1 = nullptr; // Explicitly kick off the purge - it won't happen automatically on unref dContext->performDeferredCleanup(std::chrono::milliseconds(0)); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache12View, reporter, ctxInfo) { test_12(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache12Verts, reporter, ctxInfo) { test_12(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 13: Test out the 'msNotUsed' parameter to GrContext::performDeferredCleanup. static void test_13(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); sk_sp ddl1 = helper.snap1(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); auto firstTime = GrStdSteadyClock::now(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); (helper.*addAccess)(helper.ddlCanvas2(), 2*kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), 2*kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID)); sk_sp ddl2 = helper.snap2(); ddl1 = nullptr; ddl2 = nullptr; REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2); auto secondTime = GrStdSteadyClock::now(); auto msecs = std::chrono::duration_cast(secondTime - firstTime); dContext->performDeferredCleanup(msecs); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 0, kNoID)); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache13View, reporter, ctxInfo) { test_13(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache13Verts, reporter, ctxInfo) { test_13(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert); } // Case 14: Test out mixing & matching view & vertex data w/ recycling of the cache entries to // wring out the anonymous union code. This is mainly for the MSAN bot's consumption. DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache14, reporter, ctxInfo) { constexpr int kBestPrimeNumber = 73; // palindromic in binary SkRandom rand(kBestPrimeNumber); TestHelper helper(ctxInfo.directContext()); for (int i = 0; i < 2; ++i) { SkCanvas* ddlCanvas = (!i) ? helper.ddlCanvas1() : helper.ddlCanvas2(); for (int j = 0; j < 10; ++j) { int numResources = 10*i + j + 1; int wh = numResources; if (rand.nextBool()) { helper.addViewAccess(ddlCanvas, wh, kNoID, false, false); REPORTER_ASSERT(reporter, helper.checkView(ddlCanvas, wh, /*hits*/ 0, /*misses*/ numResources, /*refs*/ 1, kNoID)); } else { helper.addVertAccess(ddlCanvas, wh, kNoID, false, false); REPORTER_ASSERT(reporter, helper.checkVert(ddlCanvas, wh, /*hits*/ 0, /*misses*/ numResources, /*refs*/ 1, kNoID)); } } if (!i) { // Drop all the accumulated resources from the thread-safe cache helper.snap1(); ctxInfo.directContext()->purgeUnlockedResources(/* scratchResourcesOnly */ false); } } } // Case 15: Test out posting invalidation messages that involve the thread safe cache static void test_15(GrDirectContext* dContext, skiatest::Reporter* reporter, TestHelper::addAccessFP addAccess, TestHelper::checkFP check, void (*create_key)(GrUniqueKey*, int wh, int id)) { TestHelper helper(dContext); (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); sk_sp ddl1 = helper.snap1(); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); GrUniqueKey key; (*create_key)(&key, kImageWH, kNoID); GrUniqueKeyInvalidatedMessage msg(key, dContext->priv().contextID(), /* inThreadSafeCache */ true); SkMessageBus::Post(msg); // This purge call is needed to process the invalidation messages dContext->purgeUnlockedResources(/* scratchResourcesOnly */ true); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0); (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false); REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID)); sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); helper.checkImage(reporter, std::move(ddl1)); helper.checkImage(reporter, std::move(ddl2)); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache15View, reporter, ctxInfo) { test_15(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView, create_view_key); } DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache15Verts, reporter, ctxInfo) { test_15(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert, create_vert_key); } // Case 16: Test out pre-emption of an existing vertex-data cache entry. This test simulates // the case where there is a race to create vertex data. However, the second one // to finish is better and usurps the first's position in the cache. // // This capability isn't available for views. static bool newer_is_always_better(SkData* /* incumbent */, SkData* /* challenger */) { return true; }; DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache16Verts, reporter, ctxInfo) { GrUniqueKey key; create_vert_key(&key, kImageWH, kNoID); TestHelper helper(ctxInfo.directContext(), newer_is_always_better); GrThreadSafeVertexTestOp* op1 = nullptr, *op2 = nullptr; helper.addVertAccess(helper.ddlCanvas1(), kImageWH, kNoID, false, false, &op1); REPORTER_ASSERT(reporter, helper.checkVert(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID)); sk_sp ddl1 = helper.snap1(); { REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); auto [vertexData, xtraData] = helper.threadSafeCache()->findVertsWithData(key); REPORTER_ASSERT(reporter, vertexData.get() == op1->vertexData()); } helper.addVertAccess(helper.ddlCanvas2(), kImageWH, kNoID, /* failLookup */ true, false, &op2); REPORTER_ASSERT(reporter, helper.checkVert(helper.ddlCanvas2(), kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID)); sk_sp ddl2 = helper.snap2(); REPORTER_ASSERT(reporter, op1->vertexData() != op2->vertexData()); { REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1); auto [vertexData, xtraData] = helper.threadSafeCache()->findVertsWithData(key); REPORTER_ASSERT(reporter, vertexData.get() == op2->vertexData()); } helper.checkImage(reporter, std::move(ddl1)); helper.checkImage(reporter, std::move(ddl2)); }