skia2/tests/GrCCPRTest.cpp
Chris Dalton 4da70190aa ccpr: Implement path mask caching
Implement caching as follows:

1) Instead of deleting the mainline ccpr atlas when finished, stash it
   away from flush to flush.

2) On subsequent flushes, check the stashed atlas to see if we can
   reuse any of its cachable paths. Copy reusable paths into 8-bit
   literal coverage atlases and store them in the resource cache.

3) Recycle the stashed atlas texture for the remaining paths in the
   flush.

Bug: skia:
Change-Id: I9b20fbea708646df1df3a5f9c044e2299706b989
Reviewed-on: https://skia-review.googlesource.com/134703
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
2018-06-18 16:14:28 +00:00

420 lines
16 KiB
C++

/*
* 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 "SkTypes.h"
#include "Test.h"
#include "GrContext.h"
#include "GrContextPriv.h"
#include "GrClip.h"
#include "GrDrawingManager.h"
#include "GrPathRenderer.h"
#include "GrPaint.h"
#include "GrRenderTargetContext.h"
#include "GrRenderTargetContextPriv.h"
#include "GrShape.h"
#include "GrTexture.h"
#include "SkMatrix.h"
#include "SkPathPriv.h"
#include "SkRect.h"
#include "sk_tool_utils.h"
#include "ccpr/GrCoverageCountingPathRenderer.h"
#include "mock/GrMockTypes.h"
#include <cmath>
static constexpr int kCanvasSize = 100;
class CCPRClip : public GrClip {
public:
CCPRClip(GrCoverageCountingPathRenderer* ccpr, const SkPath& path) : fCCPR(ccpr), fPath(path) {}
private:
bool apply(GrContext* context, GrRenderTargetContext* rtc, bool, bool, GrAppliedClip* out,
SkRect* bounds) const override {
out->addCoverageFP(fCCPR->makeClipProcessor(rtc->priv().testingOnly_getOpListID(), fPath,
SkIRect::MakeWH(rtc->width(), rtc->height()),
rtc->width(), rtc->height(),
*context->contextPriv().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;
};
enum class MarkVolatile : bool {
kNo = false,
kYes = true
};
class CCPRPathDrawer {
public:
CCPRPathDrawer(GrContext* ctx, skiatest::Reporter* reporter)
: fCtx(ctx)
, fCCPR(fCtx->contextPriv().drawingManager()->getCoverageCountingPathRenderer())
, fRTC(fCtx->contextPriv().makeDeferredRenderTargetContext(
SkBackingFit::kExact, kCanvasSize,
kCanvasSize, kRGBA_8888_GrPixelConfig,
nullptr)) {
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; }
GrCoverageCountingPathRenderer* ccpr() const { return fCCPR; }
bool valid() const { return fCCPR && fRTC; }
void clear() const { fRTC->clear(nullptr, 0, GrRenderTargetContext::CanClearFullscreen::kYes); }
void abandonGrContext() { fCtx = nullptr; fCCPR = nullptr; fRTC = nullptr; }
void drawPath(SkPath path, const SkMatrix& matrix = SkMatrix::I(),
MarkVolatile markVolatile = MarkVolatile::kYes) const {
SkASSERT(this->valid());
GrPaint paint;
paint.setColor4f(GrColor4f(0, 1, 0, 1));
GrNoClip noClip;
SkIRect clipBounds = SkIRect::MakeWH(kCanvasSize, kCanvasSize);
if (MarkVolatile::kYes == markVolatile) {
path.setIsVolatile(true);
}
GrShape shape(path);
fCCPR->drawPath({fCtx, std::move(paint), &GrUserStencilSettings::kUnused, fRTC.get(),
&noClip, &clipBounds, &matrix, &shape, GrAAType::kCoverage, false});
}
void clipFullscreenRect(SkPath clipPath, GrColor4f color = GrColor4f(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:
GrContext* fCtx;
GrCoverageCountingPathRenderer* fCCPR;
sk_sp<GrRenderTargetContext> fRTC;
};
class CCPRTest {
public:
void run(skiatest::Reporter* reporter) {
GrMockOptions mockOptions;
mockOptions.fInstanceAttribSupport = true;
mockOptions.fMapBufferFlags = GrCaps::kCanMap_MapFlag;
mockOptions.fConfigOptions[kAlpha_half_GrPixelConfig].fRenderability =
GrMockOptions::ConfigOptions::Renderability::kNonMSAA;
mockOptions.fConfigOptions[kAlpha_half_GrPixelConfig].fTexturable = true;
mockOptions.fConfigOptions[kAlpha_8_GrPixelConfig].fRenderability =
GrMockOptions::ConfigOptions::Renderability::kNonMSAA;
mockOptions.fConfigOptions[kAlpha_8_GrPixelConfig].fTexturable = true;
mockOptions.fGeometryShaderSupport = true;
mockOptions.fIntegerSupport = true;
mockOptions.fFlatInterpolationSupport = true;
this->customizeMockOptions(&mockOptions);
GrContextOptions ctxOptions;
ctxOptions.fAllowPathMaskCaching = false;
ctxOptions.fGpuPathRenderers = GpuPathRenderers::kCoverageCounting;
fMockContext = GrContext::MakeMock(&mockOptions, ctxOptions);
if (!fMockContext) {
ERRORF(reporter, "could not create mock context");
return;
}
if (!fMockContext->unique()) {
ERRORF(reporter, "mock context is not unique");
return;
}
CCPRPathDrawer ccpr(fMockContext.get(), reporter);
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 customizeMockOptions(GrMockOptions*) {}
virtual void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) = 0;
sk_sp<GrContext> fMockContext;
SkPath fPath;
};
#define DEF_CCPR_TEST(name) \
DEF_GPUTEST(name, reporter, /* options */) { \
name test; \
test.run(reporter); \
}
class GrCCPRTest_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);
}
ccpr.abandonGrContext();
REPORTER_ASSERT(reporter, !SkPathPriv::TestingOnly_unique(fPath));
fMockContext.reset();
REPORTER_ASSERT(reporter, SkPathPriv::TestingOnly_unique(fPath));
}
};
DEF_CCPR_TEST(GrCCPRTest_cleanup)
class GrCCPRTest_cleanupWithTexAllocFail : public GrCCPRTest_cleanup {
void customizeMockOptions(GrMockOptions* options) override {
options->fFailTextureAllocations = true;
}
};
DEF_CCPR_TEST(GrCCPRTest_cleanupWithTexAllocFail)
class GrCCPRTest_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.abandonGrContext();
fMockContext.reset(); // Should not crash (DrawPathsOp should have unregistered itself).
}
};
DEF_CCPR_TEST(GrCCPRTest_unregisterCulledOps)
class GrCCPRTest_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(GrCCPRTest_parseEmptyPath)
// 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 GrCCPRTest_cache : public CCPRTest {
void onRun(skiatest::Reporter* reporter, CCPRPathDrawer& ccpr) override {
static constexpr int kPathSize = 20;
SkRandom rand;
SkPath paths[200];
int primes[11] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
for (size_t i = 0; i < SK_ARRAY_COUNT(paths); ++i) {
int numPts = rand.nextRangeU(GrShape::kMaxKeyFromDataVerbCnt + 1,
GrShape::kMaxKeyFromDataVerbCnt * 2);
paths[i] = sk_tool_utils::make_star(SkRect::MakeIWH(kPathSize, kPathSize), numPts,
primes[rand.nextU() % SK_ARRAY_COUNT(primes)]);
}
SkMatrix matrices[2] = {
SkMatrix::MakeTrans(5, 5),
SkMatrix::MakeTrans(kCanvasSize - kPathSize - 5, kCanvasSize - kPathSize - 5)
};
int firstAtlasID = -1;
for (int flushIdx = 0; flushIdx < 10; ++flushIdx) {
// Draw all the paths and flush.
for (size_t i = 0; i < SK_ARRAY_COUNT(paths); ++i) {
ccpr.drawPath(paths[i], matrices[i % 2], MarkVolatile::kNo);
}
ccpr.flush();
// Figure out the mock backend ID of the atlas texture stashed away by CCPR.
GrMockTextureInfo stashedAtlasInfo;
stashedAtlasInfo.fID = -1;
const GrUniqueKey& stashedAtlasKey = ccpr.ccpr()->testingOnly_getStashedAtlasKey();
if (stashedAtlasKey.isValid()) {
GrResourceProvider* rp = ccpr.ctx()->contextPriv().resourceProvider();
sk_sp<GrSurface> stashedAtlas = rp->findByUniqueKey<GrSurface>(stashedAtlasKey);
REPORTER_ASSERT(reporter, stashedAtlas);
if (stashedAtlas) {
const auto& backendTexture = stashedAtlas->asTexture()->getBackendTexture();
backendTexture.getMockTextureInfo(&stashedAtlasInfo);
}
}
if (0 == flushIdx) {
// First flush: just note the ID of the stashed atlas and continue.
REPORTER_ASSERT(reporter, stashedAtlasKey.isValid());
firstAtlasID = stashedAtlasInfo.fID;
continue;
}
switch (flushIdx % 3) {
case 1:
// This draw should have gotten 100% cache hits; we only did integer translates
// last time (or none if it was the first flush). Therefore, no atlas should
// have been stashed away.
REPORTER_ASSERT(reporter, !stashedAtlasKey.isValid());
// Invalidate even path masks.
matrices[0].preTranslate(1.6f, 1.4f);
break;
case 2:
// Even path masks were invalidated last iteration by a subpixel translate. They
// should have been re-rendered this time and stashed away in the CCPR atlas.
REPORTER_ASSERT(reporter, stashedAtlasKey.isValid());
// 'firstAtlasID' should be kept as a scratch texture in the resource cache.
REPORTER_ASSERT(reporter, stashedAtlasInfo.fID == firstAtlasID);
// Invalidate odd path masks.
matrices[1].preTranslate(-1.4f, -1.6f);
break;
case 0:
// Odd path masks were invalidated last iteration by a subpixel translate. They
// should have been re-rendered this time and stashed away in the CCPR atlas.
REPORTER_ASSERT(reporter, stashedAtlasKey.isValid());
// 'firstAtlasID' is the same texture that got stashed away last time (assuming
// no assertion failures). So if it also got stashed this time, it means we
// first copied the even paths out of it, then recycled the exact same texture
// to render the odd paths. This is the expected behavior.
REPORTER_ASSERT(reporter, stashedAtlasInfo.fID == firstAtlasID);
// Integer translates: all path masks stay valid.
matrices[0].preTranslate(-1, -1);
matrices[1].preTranslate(1, 1);
break;
}
}
}
};
DEF_CCPR_TEST(GrCCPRTest_cache)
class CCPRRenderingTest {
public:
void run(skiatest::Reporter* reporter, GrContext* ctx) const {
if (!ctx->contextPriv().drawingManager()->getCoverageCountingPathRenderer()) {
return; // CCPR is not enabled on this GPU.
}
CCPRPathDrawer ccpr(ctx, reporter);
if (!ccpr.valid()) {
return;
}
this->onRun(reporter, ccpr);
}
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()); \
}
class GrCCPRTest_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(GrCCPRTest_busyPath)