/* * Copyright 2017 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/SkTypes.h" #include "tests/Test.h" #include "include/core/SkMatrix.h" #include "include/core/SkRect.h" #include "include/gpu/GrTexture.h" #include "include/gpu/mock/GrMockTypes.h" #include "include/private/GrRecordingContext.h" #include "src/core/SkExchange.h" #include "src/core/SkPathPriv.h" #include "src/gpu/GrClip.h" #include "src/gpu/GrContextPriv.h" #include "src/gpu/GrDrawingManager.h" #include "src/gpu/GrPaint.h" #include "src/gpu/GrPathRenderer.h" #include "src/gpu/GrRecordingContextPriv.h" #include "src/gpu/GrRenderTargetContext.h" #include "src/gpu/GrRenderTargetContextPriv.h" #include "src/gpu/ccpr/GrCCPathCache.h" #include "src/gpu/ccpr/GrCoverageCountingPathRenderer.h" #include "src/gpu/geometry/GrShape.h" #include "tools/ToolUtils.h" #include <cmath> static constexpr int kCanvasSize = 100; enum class DoCoverageCount { kNo = false, kYes }; enum class DoStroke { kNo = false, kYes }; class CCPRClip : public GrClip { public: CCPRClip(GrCoverageCountingPathRenderer* ccpr, const SkPath& path) : fCCPR(ccpr), fPath(path) {} private: bool apply(GrRecordingContext* context, GrRenderTargetContext* rtc, bool useHWAA, bool hasUserStencilSettings, GrAppliedClip* out, SkRect* bounds) const override { out->addCoverageFP(fCCPR->makeClipProcessor(rtc->priv().testingOnly_getOpListID(), fPath, SkIRect::MakeWH(rtc->width(), rtc->height()), *context->priv().caps())); return true; } bool quickContains(const SkRect&) const final { return false; } bool isRRect(const SkRect& rtBounds, SkRRect* rr, GrAA*) const final { return false; } void getConservativeBounds(int width, int height, SkIRect* rect, bool* iior) const final { rect->set(0, 0, width, height); if (iior) { *iior = false; } } GrCoverageCountingPathRenderer* const fCCPR; const SkPath fPath; }; class CCPRPathDrawer { public: CCPRPathDrawer(sk_sp<GrContext> ctx, skiatest::Reporter* reporter, DoStroke doStroke) : fCtx(ctx) , fCCPR(fCtx->priv().drawingManager()->getCoverageCountingPathRenderer()) , fRTC(fCtx->priv().makeDeferredRenderTargetContext( SkBackingFit::kExact, kCanvasSize, kCanvasSize, GrColorType::kRGBA_8888, nullptr)) , fDoStroke(DoStroke::kYes == doStroke) { if (!fCCPR) { ERRORF(reporter, "ccpr not enabled in GrContext for ccpr tests"); } if (!fRTC) { ERRORF(reporter, "failed to create GrRenderTargetContext for ccpr tests"); } } GrContext* ctx() const { return fCtx.get(); } GrCoverageCountingPathRenderer* ccpr() const { return fCCPR; } bool valid() const { return fCCPR && fRTC; } void clear() const { fRTC->clear(nullptr, SK_PMColor4fTRANSPARENT, GrRenderTargetContext::CanClearFullscreen::kYes); } void destroyGrContext() { SkASSERT(fRTC->unique()); SkASSERT(fCtx->unique()); fRTC.reset(); fCCPR = nullptr; fCtx.reset(); } void drawPath(const SkPath& path, const SkMatrix& matrix = SkMatrix::I()) const { SkASSERT(this->valid()); GrPaint paint; paint.setColor4f({ 0, 1, 0, 1 }); GrNoClip noClip; SkIRect clipBounds = SkIRect::MakeWH(kCanvasSize, kCanvasSize); GrShape shape; if (!fDoStroke) { shape = GrShape(path); } else { // Use hairlines for now, since they are the only stroke type that doesn't require a // rigid-body transform. The CCPR stroke code makes no distinction between hairlines // and regular strokes other than how it decides the device-space stroke width. SkStrokeRec stroke(SkStrokeRec::kHairline_InitStyle); stroke.setStrokeParams(SkPaint::kRound_Cap, SkPaint::kMiter_Join, 4); shape = GrShape(path, GrStyle(stroke, nullptr)); } fCCPR->testingOnly_drawPathDirectly({ fCtx.get(), std::move(paint), &GrUserStencilSettings::kUnused, fRTC.get(), &noClip, &clipBounds, &matrix, &shape, GrAAType::kCoverage, false}); } void clipFullscreenRect(SkPath clipPath, SkPMColor4f color = { 0, 1, 0, 1 }) { SkASSERT(this->valid()); GrPaint paint; paint.setColor4f(color); fRTC->drawRect(CCPRClip(fCCPR, clipPath), std::move(paint), GrAA::kYes, SkMatrix::I(), SkRect::MakeIWH(kCanvasSize, kCanvasSize)); } void flush() const { SkASSERT(this->valid()); fCtx->flush(); } private: sk_sp<GrContext> fCtx; GrCoverageCountingPathRenderer* fCCPR; sk_sp<GrRenderTargetContext> fRTC; const bool fDoStroke; }; class CCPRTest { public: void run(skiatest::Reporter* reporter, DoCoverageCount doCoverageCount, DoStroke doStroke) { GrMockOptions mockOptions; mockOptions.fInstanceAttribSupport = true; mockOptions.fHalfFloatVertexAttributeSupport = true; mockOptions.fMapBufferFlags = GrCaps::kCanMap_MapFlag; mockOptions.fConfigOptions[(int)GrColorType::kAlpha_F16].fRenderability = GrMockOptions::ConfigOptions::Renderability::kNonMSAA; mockOptions.fConfigOptions[(int)GrColorType::kAlpha_F16].fTexturable = true; mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fRenderability = GrMockOptions::ConfigOptions::Renderability::kMSAA; mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fTexturable = true; mockOptions.fGeometryShaderSupport = true; mockOptions.fIntegerSupport = true; mockOptions.fFlatInterpolationSupport = true; GrContextOptions ctxOptions; ctxOptions.fDisableCoverageCountingPaths = (DoCoverageCount::kNo == doCoverageCount); ctxOptions.fAllowPathMaskCaching = false; ctxOptions.fGpuPathRenderers = GpuPathRenderers::kCoverageCounting; this->customizeOptions(&mockOptions, &ctxOptions); sk_sp<GrContext> mockContext = GrContext::MakeMock(&mockOptions, ctxOptions); if (!mockContext) { ERRORF(reporter, "could not create mock context"); return; } if (!mockContext->unique()) { ERRORF(reporter, "mock context is not unique"); return; } CCPRPathDrawer ccpr(skstd::exchange(mockContext, nullptr), reporter, doStroke); if (!ccpr.valid()) { return; } fPath.moveTo(0, 0); fPath.cubicTo(50, 50, 0, 50, 50, 0); this->onRun(reporter, ccpr); } virtual ~CCPRTest() {} protected: virtual void customizeOptions(GrMockOptions*, GrContextOptions*) {} virtual void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) = 0; SkPath fPath; }; #define DEF_CCPR_TEST(name) \ DEF_GPUTEST(name, reporter, /* options */) { \ name test; \ test.run(reporter, DoCoverageCount::kYes, DoStroke::kNo); \ test.run(reporter, DoCoverageCount::kYes, DoStroke::kYes); \ test.run(reporter, DoCoverageCount::kNo, DoStroke::kNo); \ /* FIXME: test.run(reporter, (DoCoverageCount::kNo, DoStroke::kYes) once supported. */ \ } class CCPR_cleanup : public CCPRTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) override { REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); // Ensure paths get unreffed. for (int i = 0; i < 10; ++i) { ccpr.drawPath(fPath); } REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath)); ccpr.flush(); REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); // Ensure clip paths get unreffed. for (int i = 0; i < 10; ++i) { ccpr.clipFullscreenRect(fPath); } REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath)); ccpr.flush(); REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); // Ensure paths get unreffed when we delete the context without flushing. for (int i = 0; i < 10; ++i) { ccpr.drawPath(fPath); ccpr.clipFullscreenRect(fPath); } REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath)); ccpr.destroyGrContext(); REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); } }; DEF_CCPR_TEST(CCPR_cleanup) class CCPR_cleanupWithTexAllocFail : public CCPR_cleanup { void customizeOptions(GrMockOptions* mockOptions, GrContextOptions*) override { mockOptions->fFailTextureAllocations = true; } }; DEF_CCPR_TEST(CCPR_cleanupWithTexAllocFail) class CCPR_unregisterCulledOps : public CCPRTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) override { REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); // Ensure Ops get unregistered from CCPR when culled early. ccpr.drawPath(fPath); REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath)); ccpr.clear(); // Clear should delete the CCPR Op. REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); ccpr.flush(); // Should not crash (DrawPathsOp should have unregistered itself). // Ensure Op unregisters work when we delete the context without flushing. ccpr.drawPath(fPath); REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath)); ccpr.clear(); // Clear should delete the CCPR DrawPathsOp. REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); ccpr.destroyGrContext(); // Should not crash (DrawPathsOp should have unregistered itself). } }; DEF_CCPR_TEST(CCPR_unregisterCulledOps) class CCPR_parseEmptyPath : public CCPRTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) override { REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); // Make a path large enough that ccpr chooses to crop it by the RT bounds, and ends up with // an empty path. SkPath largeOutsidePath; largeOutsidePath.moveTo(-1e30f, -1e30f); largeOutsidePath.lineTo(-1e30f, +1e30f); largeOutsidePath.lineTo(-1e10f, +1e30f); ccpr.drawPath(largeOutsidePath); // Normally an empty path is culled before reaching ccpr, however we use a back door for // testing so this path will make it. SkPath emptyPath; SkASSERT(emptyPath.isEmpty()); ccpr.drawPath(emptyPath); // This is the test. It will exercise various internal asserts and verify we do not crash. ccpr.flush(); // Now try again with clips. ccpr.clipFullscreenRect(largeOutsidePath); ccpr.clipFullscreenRect(emptyPath); ccpr.flush(); // ... and both. ccpr.drawPath(largeOutsidePath); ccpr.clipFullscreenRect(largeOutsidePath); ccpr.drawPath(emptyPath); ccpr.clipFullscreenRect(emptyPath); ccpr.flush(); } }; DEF_CCPR_TEST(CCPR_parseEmptyPath) static int get_mock_texture_id(const GrTexture* texture) { const GrBackendTexture& backingTexture = texture->getBackendTexture(); SkASSERT(GrBackendApi::kMock == backingTexture.backend()); if (!backingTexture.isValid()) { return 0; } GrMockTextureInfo info; backingTexture.getMockTextureInfo(&info); return info.fID; } // Base class for cache path unit tests. class CCPRCacheTest : public CCPRTest { protected: // Registers as an onFlush callback in order to snag the CCPR per-flush resources and note the // texture IDs. class RecordLastMockAtlasIDs : public GrOnFlushCallbackObject { public: RecordLastMockAtlasIDs(sk_sp<GrCoverageCountingPathRenderer> ccpr) : fCCPR(ccpr) {} int lastCopyAtlasID() const { return fLastCopyAtlasID; } int lastRenderedAtlasID() const { return fLastRenderedAtlasID; } void preFlush(GrOnFlushResourceProvider*, const uint32_t* opListIDs, int numOpListIDs, SkTArray<sk_sp<GrRenderTargetContext>>* out) override { fLastRenderedAtlasID = fLastCopyAtlasID = 0; const GrCCPerFlushResources* resources = fCCPR->testingOnly_getCurrentFlushResources(); if (!resources) { return; } if (const GrTexture* tex = resources->testingOnly_frontCopyAtlasTexture()) { fLastCopyAtlasID = get_mock_texture_id(tex); } if (const GrTexture* tex = resources->testingOnly_frontRenderedAtlasTexture()) { fLastRenderedAtlasID = get_mock_texture_id(tex); } } void postFlush(GrDeferredUploadToken, const uint32_t*, int) override {} private: sk_sp<GrCoverageCountingPathRenderer> fCCPR; int fLastCopyAtlasID = 0; int fLastRenderedAtlasID = 0; }; CCPRCacheTest() { static constexpr int primes[11] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31}; SkRandom rand; for (size_t i = 0; i < SK_ARRAY_COUNT(fPaths); ++i) { int numPts = rand.nextRangeU(GrShape::kMaxKeyFromDataVerbCnt + 1, GrShape::kMaxKeyFromDataVerbCnt * 2); int step; do { step = primes[rand.nextU() % SK_ARRAY_COUNT(primes)]; } while (step == numPts); fPaths[i] = ToolUtils::make_star(SkRect::MakeLTRB(0, 0, 1, 1), numPts, step); } } void drawPathsAndFlush(CCPRPathDrawer& ccpr, const SkMatrix& m) { this->drawPathsAndFlush(ccpr, &m, 1); } void drawPathsAndFlush(CCPRPathDrawer& ccpr, const SkMatrix* matrices, int numMatrices) { // Draw all the paths. for (size_t i = 0; i < SK_ARRAY_COUNT(fPaths); ++i) { ccpr.drawPath(fPaths[i], matrices[i % numMatrices]); } // Re-draw a few paths, to test the case where a cache entry is hit more than once in a // single flush. SkRandom rand; int duplicateIndices[10]; for (size_t i = 0; i < SK_ARRAY_COUNT(duplicateIndices); ++i) { duplicateIndices[i] = rand.nextULessThan(SK_ARRAY_COUNT(fPaths)); } for (size_t i = 0; i < SK_ARRAY_COUNT(duplicateIndices); ++i) { for (size_t j = 0; j <= i; ++j) { int idx = duplicateIndices[j]; ccpr.drawPath(fPaths[idx], matrices[idx % numMatrices]); } } ccpr.flush(); } private: void customizeOptions(GrMockOptions*, GrContextOptions* ctxOptions) override { ctxOptions->fAllowPathMaskCaching = true; } void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) final { RecordLastMockAtlasIDs atlasIDRecorder(sk_ref_sp(ccpr.ccpr())); ccpr.ctx()->priv().addOnFlushCallbackObject(&atlasIDRecorder); this->onRun(reporter, ccpr, atlasIDRecorder); ccpr.ctx()->priv().testingOnly_flushAndRemoveOnFlushCallbackObject(&atlasIDRecorder); } virtual void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs&) = 0; protected: SkPath fPaths[350]; }; // Ensures ccpr always reuses the same atlas texture in the animation use case. class CCPR_cache_animationAtlasReuse : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { SkMatrix m = SkMatrix::MakeTrans(kCanvasSize/2, kCanvasSize/2); m.preScale(80, 80); m.preTranslate(-.5,-.5); this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); const int atlasID = atlasIDRecorder.lastRenderedAtlasID(); // Ensures we always reuse the same atlas texture in the animation use case. for (int i = 0; i < 12; ++i) { // 59 is prime, so we will hit every integer modulo 360 before repeating. m.preRotate(59, .5, .5); // Go twice. Paths have to get drawn twice with the same matrix before we cache their // atlas. This makes sure that on the subsequent draw, after an atlas has been cached // and is then invalidated since the matrix will change, that the same underlying // texture object is still reused for the next atlas. for (int j = 0; j < 2; ++j) { this->drawPathsAndFlush(ccpr, m); // Nothing should be copied to an 8-bit atlas after just two draws. REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, atlasIDRecorder.lastRenderedAtlasID() == atlasID); } } // Do the last draw again. (On draw 3 they should get copied to an 8-bit atlas.) this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); // Now double-check that everything continues to hit the cache as expected when the matrix // doesn't change. for (int i = 0; i < 10; ++i) { this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } } }; DEF_CCPR_TEST(CCPR_cache_animationAtlasReuse) class CCPR_cache_recycleEntries : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { SkMatrix m = SkMatrix::MakeTrans(kCanvasSize/2, kCanvasSize/2); m.preScale(80, 80); m.preTranslate(-.5,-.5); auto cache = ccpr.ccpr()->testingOnly_getPathCache(); REPORTER_ASSERT(reporter, cache); const auto& lru = cache->testingOnly_getLRU(); SkTArray<const void*> expectedPtrs; // Ensures we always reuse the same atlas texture in the animation use case. for (int i = 0; i < 5; ++i) { // 59 is prime, so we will hit every integer modulo 360 before repeating. m.preRotate(59, .5, .5); // Go twice. Paths have to get drawn twice with the same matrix before we cache their // atlas. for (int j = 0; j < 2; ++j) { this->drawPathsAndFlush(ccpr, m); // Nothing should be copied to an 8-bit atlas after just two draws. REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } int idx = 0; for (const GrCCPathCacheEntry* entry : lru) { if (0 == i) { expectedPtrs.push_back(entry); } else { // The same pointer should have been recycled for the new matrix. REPORTER_ASSERT(reporter, entry == expectedPtrs[idx]); } ++idx; } } } }; DEF_CCPR_TEST(CCPR_cache_recycleEntries) // Ensures mostly-visible paths get their full mask cached. class CCPR_cache_mostlyVisible : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { SkMatrix matrices[3] = { SkMatrix::MakeScale(kCanvasSize/2, kCanvasSize/2), // Fully visible. SkMatrix::MakeScale(kCanvasSize * 1.25, kCanvasSize * 1.25), // Mostly visible. SkMatrix::MakeScale(kCanvasSize * 1.5, kCanvasSize * 1.5), // Mostly NOT visible. }; for (int i = 0; i < 10; ++i) { this->drawPathsAndFlush(ccpr, matrices, 3); if (2 == i) { // The mostly-visible paths should still get cached. REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); } // Ensure mostly NOT-visible paths never get cached. REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } // Clear the path cache. this->drawPathsAndFlush(ccpr, SkMatrix::I()); // Now only draw the fully/mostly visible ones. for (int i = 0; i < 2; ++i) { this->drawPathsAndFlush(ccpr, matrices, 2); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } // On draw 3 they should get copied to an 8-bit atlas. this->drawPathsAndFlush(ccpr, matrices, 2); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); for (int i = 0; i < 10; ++i) { this->drawPathsAndFlush(ccpr, matrices, 2); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } // Draw a different part of the path to ensure the full mask was cached. matrices[1].postTranslate(SkScalarFloorToInt(kCanvasSize * -.25f), SkScalarFloorToInt(kCanvasSize * -.25f)); for (int i = 0; i < 10; ++i) { this->drawPathsAndFlush(ccpr, matrices, 2); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } } }; DEF_CCPR_TEST(CCPR_cache_mostlyVisible) // Ensures GrContext::performDeferredCleanup works. class CCPR_cache_deferredCleanup : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { SkMatrix m = SkMatrix::MakeScale(20, 20); int lastRenderedAtlasID = 0; for (int i = 0; i < 5; ++i) { this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); int renderedAtlasID = atlasIDRecorder.lastRenderedAtlasID(); REPORTER_ASSERT(reporter, renderedAtlasID != lastRenderedAtlasID); lastRenderedAtlasID = renderedAtlasID; this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, lastRenderedAtlasID == atlasIDRecorder.lastRenderedAtlasID()); // On draw 3 they should get copied to an 8-bit atlas. this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); for (int i = 0; i < 10; ++i) { this->drawPathsAndFlush(ccpr, m); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } ccpr.ctx()->performDeferredCleanup(std::chrono::milliseconds(0)); } } }; DEF_CCPR_TEST(CCPR_cache_deferredCleanup) // Verifies the cache/hash table internals. class CCPR_cache_hashTable : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { using CoverageType = GrCCAtlas::CoverageType; SkMatrix m = SkMatrix::MakeScale(20, 20); for (int i = 0; i < 5; ++i) { this->drawPathsAndFlush(ccpr, m); if (2 == i) { REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); } if (i < 2) { REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } auto cache = ccpr.ccpr()->testingOnly_getPathCache(); REPORTER_ASSERT(reporter, cache); const auto& hash = cache->testingOnly_getHashTable(); const auto& lru = cache->testingOnly_getLRU(); int count = 0; for (GrCCPathCacheEntry* entry : lru) { auto* node = hash.find(entry->cacheKey()); REPORTER_ASSERT(reporter, node); REPORTER_ASSERT(reporter, node->entry() == entry); REPORTER_ASSERT(reporter, 0 == entry->testingOnly_peekOnFlushRefCnt()); REPORTER_ASSERT(reporter, entry->unique()); if (0 == i) { REPORTER_ASSERT(reporter, !entry->cachedAtlas()); } else { const GrCCCachedAtlas* cachedAtlas = entry->cachedAtlas(); REPORTER_ASSERT(reporter, cachedAtlas); if (1 == i) { REPORTER_ASSERT(reporter, ccpr.ccpr()->coverageType() == cachedAtlas->coverageType()); } else { REPORTER_ASSERT(reporter, CoverageType::kA8_LiteralCoverage == cachedAtlas->coverageType()); } REPORTER_ASSERT(reporter, cachedAtlas->textureKey().isValid()); // The actual proxy should not be held past the end of a flush. REPORTER_ASSERT(reporter, !cachedAtlas->getOnFlushProxy()); REPORTER_ASSERT(reporter, 0 == cachedAtlas->testingOnly_peekOnFlushRefCnt()); } ++count; } REPORTER_ASSERT(reporter, hash.count() == count); } } }; DEF_CCPR_TEST(CCPR_cache_hashTable) // Ensures paths get cached even when using a sporadic flushing pattern and drawing out of order // (a la Chrome tiles). class CCPR_cache_multiFlush : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { static constexpr int kNumPaths = SK_ARRAY_COUNT(fPaths); static constexpr int kBigPrimes[] = { 9323, 11059, 22993, 38749, 45127, 53147, 64853, 77969, 83269, 99989}; SkRandom rand; SkMatrix m = SkMatrix::I(); for (size_t i = 0; i < SK_ARRAY_COUNT(kBigPrimes); ++i) { int prime = kBigPrimes[i]; int endPathIdx = (int)rand.nextULessThan(kNumPaths); int pathIdx = endPathIdx; int nextFlush = rand.nextRangeU(1, 47); for (int j = 0; j < kNumPaths; ++j) { pathIdx = (pathIdx + prime) % kNumPaths; int repeat = rand.nextRangeU(1, 3); for (int k = 0; k < repeat; ++k) { ccpr.drawPath(fPaths[pathIdx], m); } if (nextFlush == j) { ccpr.flush(); // The paths are small enough that we should never copy to an A8 atlas. REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); if (i < 2) { REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } nextFlush = SkTMin(j + (int)rand.nextRangeU(1, 29), kNumPaths - 1); } } SkASSERT(endPathIdx == pathIdx % kNumPaths); } } }; DEF_CCPR_TEST(CCPR_cache_multiFlush) // Ensures a path drawn over mutiple tiles gets cached. class CCPR_cache_multiTileCache : public CCPRCacheTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { // Make sure a path drawn over 9 tiles gets cached (1 tile out of 9 is >10% visibility). const SkMatrix m0 = SkMatrix::MakeScale(kCanvasSize*3, kCanvasSize*3); const SkPath p0 = fPaths[0]; for (int i = 0; i < 9; ++i) { static constexpr int kRowOrder[9] = {0,1,1,0,2,2,2,1,0}; static constexpr int kColumnOrder[9] = {0,0,1,1,0,1,2,2,2}; SkMatrix tileM = m0; tileM.postTranslate(-kCanvasSize * kColumnOrder[i], -kCanvasSize * kRowOrder[i]); ccpr.drawPath(p0, tileM); ccpr.flush(); if (i < 5) { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } else if (5 == i) { REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); } } // Now make sure paths don't get cached when visibility is <10% for every draw (12 tiles). const SkMatrix m1 = SkMatrix::MakeScale(kCanvasSize*4, kCanvasSize*3); const SkPath p1 = fPaths[1]; for (int row = 0; row < 3; ++row) { for (int col = 0; col < 4; ++col) { SkMatrix tileM = m1; tileM.postTranslate(-kCanvasSize * col, -kCanvasSize * row); ccpr.drawPath(p1, tileM); ccpr.flush(); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } } // Double-check the cache is still intact. ccpr.drawPath(p0, m0); ccpr.flush(); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); ccpr.drawPath(p1, m1); ccpr.flush(); REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastRenderedAtlasID()); } }; DEF_CCPR_TEST(CCPR_cache_multiTileCache) // This test exercises CCPR's cache capabilities by drawing many paths with two different // transformation matrices. We then vary the matrices independently by whole and partial pixels, // and verify the caching behaved as expected. class CCPR_cache_partialInvalidate : public CCPRCacheTest { void customizeOptions(GrMockOptions*, GrContextOptions* ctxOptions) override { ctxOptions->fAllowPathMaskCaching = true; } static constexpr int kPathSize = 4; void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr, const RecordLastMockAtlasIDs& atlasIDRecorder) override { SkMatrix matrices[2] = { SkMatrix::MakeTrans(5, 5), SkMatrix::MakeTrans(kCanvasSize - kPathSize - 5, kCanvasSize - kPathSize - 5) }; matrices[0].preScale(kPathSize, kPathSize); matrices[1].preScale(kPathSize, kPathSize); int firstAtlasID = 0; for (int iterIdx = 0; iterIdx < 4*3*2; ++iterIdx) { this->drawPathsAndFlush(ccpr, matrices, 2); if (0 == iterIdx) { // First iteration: just note the ID of the stashed atlas and continue. firstAtlasID = atlasIDRecorder.lastRenderedAtlasID(); REPORTER_ASSERT(reporter, 0 != firstAtlasID); continue; } int testIdx = (iterIdx/2) % 3; int repetitionIdx = iterIdx % 2; switch (testIdx) { case 0: if (0 == repetitionIdx) { // This is the big test. New paths were drawn twice last round. On hit 2 // (last time), 'firstAtlasID' was cached as a 16-bit atlas. Now, on hit 3, // these paths should be copied out of 'firstAtlasID', and into an A8 atlas. // THEN: we should recycle 'firstAtlasID' and reuse that same texture to // render the new masks. REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); REPORTER_ASSERT(reporter, atlasIDRecorder.lastRenderedAtlasID() == firstAtlasID); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); // This is hit 2 for the new masks. Next time they will be copied to an A8 // atlas. REPORTER_ASSERT(reporter, atlasIDRecorder.lastRenderedAtlasID() == firstAtlasID); } if (1 == repetitionIdx) { // Integer translates: all path masks stay valid. matrices[0].preTranslate(-1, -1); matrices[1].preTranslate(1, 1); } break; case 1: if (0 == repetitionIdx) { // New paths were drawn twice last round. The third hit (now) they should be // copied to an A8 atlas. REPORTER_ASSERT(reporter, 0 != atlasIDRecorder.lastCopyAtlasID()); } else { REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); } // This draw should have gotten 100% cache hits; we only did integer translates // last time (or none if it was the first flush). Therefore, everything should // have been cached. REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastRenderedAtlasID()); if (1 == repetitionIdx) { // Invalidate even path masks. matrices[0].preTranslate(1.6f, 1.4f); } break; case 2: // No new masks to copy from last time; it had 100% cache hits. REPORTER_ASSERT(reporter, 0 == atlasIDRecorder.lastCopyAtlasID()); // Even path masks were invalidated last iteration by a subpixel translate. // They should have been re-rendered this time in the original 'firstAtlasID' // texture. REPORTER_ASSERT(reporter, atlasIDRecorder.lastRenderedAtlasID() == firstAtlasID); if (1 == repetitionIdx) { // Invalidate odd path masks. matrices[1].preTranslate(-1.4f, -1.6f); } break; } } } }; DEF_CCPR_TEST(CCPR_cache_partialInvalidate) class CCPR_unrefPerOpListPathsBeforeOps : public CCPRTest { void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) override { REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); for (int i = 0; i < 10000; ++i) { // Draw enough paths to make the arena allocator hit the heap. ccpr.drawPath(fPath); } // Unref the GrCCPerOpListPaths object. auto perOpListPathsMap = ccpr.ccpr()->detachPendingPaths(); perOpListPathsMap.clear(); // Now delete the Op and all its draws. REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath)); ccpr.flush(); REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath)); } }; DEF_CCPR_TEST(CCPR_unrefPerOpListPathsBeforeOps) class CCPRRenderingTest { public: void run(skiatest::Reporter* reporter, GrContext* ctx, DoStroke doStroke) const { if (auto ccpr = ctx->priv().drawingManager()->getCoverageCountingPathRenderer()) { if (DoStroke::kYes == doStroke && GrCCAtlas::CoverageType::kA8_Multisample == ccpr->coverageType()) { return; // Stroking is not yet supported for multisample. } CCPRPathDrawer drawer(sk_ref_sp(ctx), reporter, doStroke); if (!drawer.valid()) { return; } this->onRun(reporter, drawer); } } virtual ~CCPRRenderingTest() {} protected: virtual void onRun(skiatest::Reporter* reporter, const CCPRPathDrawer& ccpr) const = 0; }; #define DEF_CCPR_RENDERING_TEST(name) \ DEF_GPUTEST_FOR_RENDERING_CONTEXTS(name, reporter, ctxInfo) { \ name test; \ test.run(reporter, ctxInfo.grContext(), DoStroke::kNo); \ test.run(reporter, ctxInfo.grContext(), DoStroke::kYes); \ } class CCPR_busyPath : public CCPRRenderingTest { void onRun(skiatest::Reporter* reporter, const CCPRPathDrawer& ccpr) const override { static constexpr int kNumBusyVerbs = 1 << 17; ccpr.clear(); SkPath busyPath; busyPath.moveTo(0, 0); // top left busyPath.lineTo(kCanvasSize, kCanvasSize); // bottom right for (int i = 2; i < kNumBusyVerbs; ++i) { float offset = i * ((float)kCanvasSize / kNumBusyVerbs); busyPath.lineTo(kCanvasSize - offset, kCanvasSize + offset); // offscreen } ccpr.drawPath(busyPath); ccpr.flush(); // If this doesn't crash, the test passed. // If it does, maybe fiddle with fMaxInstancesPerDrawArraysWithoutCrashing in // your platform's GrGLCaps. } }; DEF_CCPR_RENDERING_TEST(CCPR_busyPath)