skia2/bench/TessellateBench.cpp

505 lines
20 KiB
C++
Raw Normal View History

/*
* 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 "bench/Benchmark.h"
#include "include/gpu/GrDirectContext.h"
#include "src/core/SkPathPriv.h"
#include "src/gpu/GrDirectContextPriv.h"
#include "src/gpu/GrOpFlushState.h"
Use Wang's formula for quadratic and cubic point counts - most of the small diffs are because I moved GrWangsFormula.h out of the tessellate/ directory and into the geometry/ directory since it's more general than HW tessellation. The previous implementation was based on the heuristic that the distance from the true curve to the line segment would be divided by 4 every time the curve was recursively subdivided. This was a reasonable approximation if the curve had balanced curvature on both sides of the split. However, in the case of the new GM's curve, the left half was already very linear and the right half had much higher curves. This lead to the approximation reporting fewer points than required. Theoretically, those few points that weren't utilized by the left half of the curve could have been made available to the right half, but the implementation of that would be tricky. Instead, it now uses Wang's formula to compute the number of points. Since recursive subdivision leads to linearly spaced samples assuming it can't stop early, this point count represents a valid upper bound on what's needed. It also then ensures both left and right halves of a curve have the point counts they might need w/o updating the generation implementations. However, since the recursive point generation exits once each section has reached the error tolerance, in scenarios where the prior approximation was reasonable, we'll end up using fewer points than reported by Wang's. Hopefully that means there is negligible performance regression since we won't be increasing vertex counts by that much (except where needed for correctness). Bug: skia:11886 Change-Id: Iba39dbe4de82011775524583efd461b10c9259fe Reviewed-on: https://skia-review.googlesource.com/c/skia/+/405197 Reviewed-by: Chris Dalton <csmartdalton@google.com> Reviewed-by: Brian Salomon <bsalomon@google.com> Commit-Queue: Michael Ludwig <michaelludwig@google.com>
2021-05-11 14:00:12 +00:00
#include "src/gpu/geometry/GrWangsFormula.h"
#include "src/gpu/mock/GrMockOpTarget.h"
#include "src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h"
#include "src/gpu/tessellate/GrPathTessellator.h"
#include "src/gpu/tessellate/GrStrokeFixedCountTessellator.h"
#include "src/gpu/tessellate/GrStrokeHardwareTessellator.h"
#include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
#include "tools/ToolUtils.h"
#include <vector>
using ShaderFlags = GrStrokeTessellateShader::ShaderFlags;
// This is the number of cubics in desk_chalkboard.skp. (There are no quadratics in the chalkboard.)
constexpr static int kNumCubicsInChalkboard = 47182;
static sk_sp<GrDirectContext> make_mock_context() {
GrMockOptions mockOptions;
mockOptions.fDrawInstancedSupport = true;
mockOptions.fMaxTessellationSegments = 64;
mockOptions.fMapBufferFlags = GrCaps::kCanMap_MapFlag;
mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fRenderability =
GrMockOptions::ConfigOptions::Renderability::kMSAA;
mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fTexturable = true;
mockOptions.fIntegerSupport = true;
GrContextOptions ctxOptions;
ctxOptions.fGpuPathRenderers = GpuPathRenderers::kTessellation;
ctxOptions.fEnableExperimentalHardwareTessellation = true;
return GrDirectContext::MakeMock(&mockOptions, ctxOptions);
}
static SkPath make_cubic_path() {
SkRandom rand;
SkPath path;
for (int i = 0; i < kNumCubicsInChalkboard/2; ++i) {
float x = std::ldexp(rand.nextF(), (i % 18)) / 1e3f;
path.cubicTo(111.625f*x, 308.188f*x, 764.62f*x, -435.688f*x, 742.63f*x, 85.187f*x);
path.cubicTo(764.62f*x, -435.688f*x, 111.625f*x, 308.188f*x, 0, 0);
}
return path;
}
static SkPath make_conic_path() {
SkRandom rand;
SkPath path;
for (int i = 0; i < kNumCubicsInChalkboard / 40; ++i) {
for (int j = -10; j <= 10; j++) {
const float x = std::ldexp(rand.nextF(), (i % 18)) / 1e3f;
const float w = std::ldexp(1 + rand.nextF(), j);
path.conicTo(111.625f * x, 308.188f * x, 764.62f * x, -435.688f * x, w);
}
}
return path;
}
// This serves as a base class for benchmarking individual methods on GrPathTessellateOp.
class PathTessellateBenchmark : public Benchmark {
public:
PathTessellateBenchmark(const char* subName, const SkPath& p, const SkMatrix& m)
: fPath(p), fMatrix(m) {
fName.printf("tessellate_%s", subName);
}
const char* onGetName() override { return fName.c_str(); }
bool isSuitableFor(Backend backend) final { return backend == kNonRendering_Backend; }
protected:
void onDelayedSetup() override {
fTarget = std::make_unique<GrMockOpTarget>(make_mock_context());
}
void onDraw(int loops, SkCanvas*) final {
if (!fTarget->mockContext()) {
SkDebugf("ERROR: could not create mock context.");
return;
}
for (int i = 0; i < loops; ++i) {
this->runBench();
fTarget->resetAllocator();
}
}
virtual void runBench() = 0;
SkString fName;
std::unique_ptr<GrMockOpTarget> fTarget;
const SkPath fPath;
const SkMatrix fMatrix;
};
#define DEF_PATH_TESS_BENCH(NAME, PATH, MATRIX) \
class PathTessellateBenchmark_##NAME : public PathTessellateBenchmark { \
public: \
PathTessellateBenchmark_##NAME() : PathTessellateBenchmark(#NAME, (PATH), (MATRIX)) {} \
void runBench() override; \
}; \
DEF_BENCH( return new PathTessellateBenchmark_##NAME(); ); \
void PathTessellateBenchmark_##NAME::runBench()
DEF_PATH_TESS_BENCH(GrPathIndirectTessellator, make_cubic_path(), SkMatrix::I()) {
GrPathIndirectTessellator tess(fMatrix, fPath, GrPathIndirectTessellator::DrawInnerFan::kNo);
tess.prepare(fTarget.get(), fMatrix, fPath, nullptr);
}
DEF_PATH_TESS_BENCH(GrPathOuterCurveTessellator, make_cubic_path(), SkMatrix::I()) {
GrPathOuterCurveTessellator tess;
tess.prepare(fTarget.get(), fMatrix, fPath, nullptr);
}
DEF_PATH_TESS_BENCH(GrPathWedgeTessellator, make_cubic_path(), SkMatrix::I()) {
GrPathWedgeTessellator tess;
tess.prepare(fTarget.get(), fMatrix, fPath, nullptr);
}
static void benchmark_wangs_formula_cubic_log2(const SkMatrix& matrix, const SkPath& path) {
int sum = 0;
GrVectorXform xform(matrix);
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
if (verb == SkPathVerb::kCubic) {
sum += GrWangsFormula::cubic_log2(4, pts, xform);
}
}
// Don't let the compiler optimize away GrWangsFormula::cubic_log2.
if (sum <= 0) {
SK_ABORT("sum should be > 0.");
}
}
DEF_PATH_TESS_BENCH(wangs_formula_cubic_log2, make_cubic_path(), SkMatrix::I()) {
benchmark_wangs_formula_cubic_log2(fMatrix, fPath);
}
DEF_PATH_TESS_BENCH(wangs_formula_cubic_log2_scale, make_cubic_path(),
SkMatrix::Scale(1.1f, 0.9f)) {
benchmark_wangs_formula_cubic_log2(fMatrix, fPath);
}
DEF_PATH_TESS_BENCH(wangs_formula_cubic_log2_affine, make_cubic_path(),
SkMatrix::MakeAll(.9f,0.9f,0, 1.1f,1.1f,0, 0,0,1)) {
benchmark_wangs_formula_cubic_log2(fMatrix, fPath);
}
static void benchmark_wangs_formula_conic(const SkMatrix& matrix, const SkPath& path) {
// Conic version expects tolerance, not "precision"
constexpr float kTolerance = 4;
int sum = 0;
GrVectorXform xform(matrix);
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
if (verb == SkPathVerb::kConic) {
sum += GrWangsFormula::conic(kTolerance, pts, *w, xform);
}
}
// Don't let the compiler optimize away GrWangsFormula::conic.
if (sum <= 0) {
SK_ABORT("sum should be > 0.");
}
}
static void benchmark_wangs_formula_conic_log2(const SkMatrix& matrix, const SkPath& path) {
// Conic version expects tolerance, not "precision"
constexpr float kTolerance = 4;
int sum = 0;
GrVectorXform xform(matrix);
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
if (verb == SkPathVerb::kConic) {
sum += GrWangsFormula::conic_log2(kTolerance, pts, *w, xform);
}
}
// Don't let the compiler optimize away GrWangsFormula::conic.
if (sum <= 0) {
SK_ABORT("sum should be > 0.");
}
}
DEF_PATH_TESS_BENCH(wangs_formula_conic, make_conic_path(), SkMatrix::I()) {
benchmark_wangs_formula_conic(fMatrix, fPath);
}
DEF_PATH_TESS_BENCH(wangs_formula_conic_log2, make_conic_path(), SkMatrix::I()) {
benchmark_wangs_formula_conic_log2(fMatrix, fPath);
}
DEF_PATH_TESS_BENCH(middle_out_triangulation,
ToolUtils::make_star(SkRect::MakeWH(500, 500), kNumCubicsInChalkboard),
SkMatrix::I()) {
sk_sp<const GrBuffer> buffer;
int baseVertex;
GrVertexWriter vertexWriter = static_cast<SkPoint*>(fTarget->makeVertexSpace(
sizeof(SkPoint), kNumCubicsInChalkboard, &buffer, &baseVertex));
GrMiddleOutPolygonTriangulator::WritePathInnerFan(&vertexWriter, 3, fPath);
}
using PathStrokeList = GrStrokeTessellator::PathStrokeList;
using MakeTessellatorFn = std::unique_ptr<GrStrokeTessellator>(*)(ShaderFlags, const SkMatrix&,
PathStrokeList*,
const GrShaderCaps&);
static std::unique_ptr<GrStrokeTessellator> make_hw_tessellator(ShaderFlags shaderFlags,
const SkMatrix& viewMatrix,
PathStrokeList* pathStrokeList,
const GrShaderCaps& shaderCaps) {
return std::make_unique<GrStrokeHardwareTessellator>(shaderFlags, viewMatrix, pathStrokeList,
shaderCaps);
}
static std::unique_ptr<GrStrokeTessellator> make_fixed_count_tessellator(
ShaderFlags shaderFlags, const SkMatrix& viewMatrix, PathStrokeList* pathStrokeList,
const GrShaderCaps& shaderCaps) {
return std::make_unique<GrStrokeFixedCountTessellator>(shaderFlags, viewMatrix, pathStrokeList);
}
using MakePathStrokesFn = std::vector<PathStrokeList>(*)();
static std::vector<PathStrokeList> make_simple_cubic_path() {
auto path = SkPath().moveTo(0, 0);
for (int i = 0; i < kNumCubicsInChalkboard/2; ++i) {
path.cubicTo(100, 0, 50, 100, 100, 100);
path.cubicTo(0, -100, 200, 100, 0, 0);
}
SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle);
stroke.setStrokeStyle(8);
stroke.setStrokeParams(SkPaint::kButt_Cap, SkPaint::kMiter_Join, 4);
return {{path, stroke, SK_PMColor4fWHITE}};
}
// Generates a list of paths that resemble the MotionMark benchmark.
static std::vector<PathStrokeList> make_motionmark_paths() {
std::vector<PathStrokeList> pathStrokes;
SkRandom rand;
for (int i = 0; i < 8702; ++i) {
// The number of paths with a given number of verbs in the MotionMark bench gets cut in half
// every time the number of verbs increases by 1.
int numVerbs = 28 - SkNextLog2(rand.nextRangeU(0, (1 << 27) - 1));
SkPath path;
for (int j = 0; j < numVerbs; ++j) {
switch (rand.nextU() & 3) {
case 0:
case 1:
path.lineTo(rand.nextRangeF(0, 150), rand.nextRangeF(0, 150));
break;
case 2:
if (rand.nextULessThan(10) == 0) {
// Cusp.
auto [x, y] = (path.isEmpty())
? SkPoint{0,0}
: SkPathPriv::PointData(path)[path.countPoints() - 1];
path.quadTo(x + rand.nextRangeF(0, 150), y, x - rand.nextRangeF(0, 150), y);
} else {
path.quadTo(rand.nextRangeF(0, 150), rand.nextRangeF(0, 150),
rand.nextRangeF(0, 150), rand.nextRangeF(0, 150));
}
break;
case 3:
if (rand.nextULessThan(10) == 0) {
// Cusp.
float y = (path.isEmpty())
? 0 : SkPathPriv::PointData(path)[path.countPoints() - 1].fY;
path.cubicTo(rand.nextRangeF(0, 150), y, rand.nextRangeF(0, 150), y,
rand.nextRangeF(0, 150), y);
} else {
path.cubicTo(rand.nextRangeF(0, 150), rand.nextRangeF(0, 150),
rand.nextRangeF(0, 150), rand.nextRangeF(0, 150),
rand.nextRangeF(0, 150), rand.nextRangeF(0, 150));
}
break;
}
}
SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle);
// The number of paths with a given stroke width in the MotionMark bench gets cut in half
// every time the stroke width increases by 1.
float strokeWidth = 21 - log2f(rand.nextRangeF(0, 1 << 20));
stroke.setStrokeStyle(strokeWidth);
stroke.setStrokeParams(SkPaint::kButt_Cap, SkPaint::kBevel_Join, 0);
pathStrokes.emplace_back(path, stroke, SK_PMColor4fWHITE);
}
return pathStrokes;
}
class TessPrepareBench : public Benchmark {
public:
TessPrepareBench(MakePathStrokesFn makePathStrokesFn, MakeTessellatorFn makeTessellatorFn,
ShaderFlags shaderFlags, float matrixScale, const char* suffix)
: fMakePathStrokesFn(makePathStrokesFn)
, fMakeTessellatorFn(makeTessellatorFn)
, fShaderFlags(shaderFlags)
, fMatrixScale(matrixScale) {
fName.printf("tessellate_%s", suffix);
}
private:
const char* onGetName() override { return fName.c_str(); }
bool isSuitableFor(Backend backend) final { return backend == kNonRendering_Backend; }
void onDelayedSetup() override {
fTarget = std::make_unique<GrMockOpTarget>(make_mock_context());
if (!fTarget->mockContext()) {
SkDebugf("ERROR: could not create mock context.");
return;
}
fPathStrokes = fMakePathStrokesFn();
for (size_t i = 0; i < fPathStrokes.size(); ++i) {
if (i + 1 < fPathStrokes.size()) {
fPathStrokes[i].fNext = &fPathStrokes[i + 1];
}
fTotalVerbCount += fPathStrokes[i].fPath.countVerbs();
}
fTessellator = fMakeTessellatorFn(fShaderFlags, SkMatrix::Scale(fMatrixScale, fMatrixScale),
fPathStrokes.data(), *fTarget->caps().shaderCaps());
}
void onDraw(int loops, SkCanvas*) final {
for (int i = 0; i < loops; ++i) {
fTessellator->prepare(fTarget.get(), fTotalVerbCount);
fTarget->resetAllocator();
}
}
SkString fName;
MakePathStrokesFn fMakePathStrokesFn;
MakeTessellatorFn fMakeTessellatorFn;
const ShaderFlags fShaderFlags;
float fMatrixScale;
std::unique_ptr<GrMockOpTarget> fTarget;
std::vector<PathStrokeList> fPathStrokes;
std::unique_ptr<GrStrokeTessellator> fTessellator;
SkArenaAlloc fPersistentArena{1024};
int fTotalVerbCount = 0;
};
DEF_BENCH(return new TessPrepareBench(
make_simple_cubic_path, make_hw_tessellator, ShaderFlags::kNone, 1,
"GrStrokeHardwareTessellator");
)
DEF_BENCH(return new TessPrepareBench(
make_simple_cubic_path, make_hw_tessellator, ShaderFlags::kNone, 5,
"GrStrokeHardwareTessellator_one_chop");
)
DEF_BENCH(return new TessPrepareBench(
make_motionmark_paths, make_hw_tessellator, ShaderFlags::kDynamicStroke, 1,
"GrStrokeHardwareTessellator_motionmark");
)
DEF_BENCH(return new TessPrepareBench(
make_simple_cubic_path, make_fixed_count_tessellator, ShaderFlags::kNone, 1,
"GrStrokeFixedCountTessellator");
)
DEF_BENCH(return new TessPrepareBench(
make_simple_cubic_path, make_fixed_count_tessellator, ShaderFlags::kNone, 5,
"GrStrokeFixedCountTessellator_one_chop");
)
DEF_BENCH(return new TessPrepareBench(
make_motionmark_paths, make_fixed_count_tessellator, ShaderFlags::kDynamicStroke, 1,
"GrStrokeFixedCountTessellator_motionmark");
)
class GrStrokeIndirectTessellator::Benchmark : public ::Benchmark {
protected:
Benchmark(const char* nameSuffix, SkPaint::Join join) : fJoin(join) {
fName.printf("tessellate_GrStrokeIndirectTessellator%s", nameSuffix);
}
const SkPaint::Join fJoin;
private:
const char* onGetName() final { return fName.c_str(); }
bool isSuitableFor(Backend backend) final { return backend == kNonRendering_Backend; }
void onDelayedSetup() final {
fTarget = std::make_unique<GrMockOpTarget>(make_mock_context());
fStrokeRec.setStrokeStyle(8);
fStrokeRec.setStrokeParams(SkPaint::kButt_Cap, fJoin, 4);
this->setupPaths(&fPaths);
}
void onDraw(int loops, SkCanvas*) final {
if (!fTarget->mockContext()) {
SkDebugf("ERROR: could not create mock context.");
return;
}
for (int i = 0; i < loops; ++i) {
for (const SkPath& path : fPaths) {
GrStrokeTessellator::PathStrokeList pathStroke(path, fStrokeRec, SK_PMColor4fWHITE);
GrStrokeIndirectTessellator tessellator(ShaderFlags::kNone, SkMatrix::I(),
&pathStroke, path.countVerbs(),
fTarget->allocator());
tessellator.prepare(fTarget.get(), path.countVerbs());
}
fTarget->resetAllocator();
}
}
virtual void setupPaths(SkTArray<SkPath>*) = 0;
SkString fName;
std::unique_ptr<GrMockOpTarget> fTarget;
SkTArray<SkPath> fPaths;
SkStrokeRec fStrokeRec{SkStrokeRec::kHairline_InitStyle};
};
class StrokeIndirectBenchmark : public GrStrokeIndirectTessellator::Benchmark {
public:
StrokeIndirectBenchmark(const char* nameSuffix, SkPaint::Join join, std::vector<SkPoint> pts)
: Benchmark(nameSuffix, join), fPts(std::move(pts)) {}
private:
void setupPaths(SkTArray<SkPath>* paths) final {
SkPath& path = paths->push_back();
if (fJoin == SkPaint::kRound_Join) {
path.reset().moveTo(fPts.back());
for (size_t i = 0; i < kNumCubicsInChalkboard/fPts.size(); ++i) {
for (size_t j = 0; j < fPts.size(); ++j) {
path.lineTo(fPts[j]);
}
}
} else {
path.reset().moveTo(fPts[0]);
for (int i = 0; i < kNumCubicsInChalkboard/2; ++i) {
if (fPts.size() == 4) {
path.cubicTo(fPts[1], fPts[2], fPts[3]);
path.cubicTo(fPts[2], fPts[1], fPts[0]);
} else {
SkASSERT(fPts.size() == 3);
path.quadTo(fPts[1], fPts[2]);
path.quadTo(fPts[2], fPts[1]);
}
}
}
}
const std::vector<SkPoint> fPts;
};
DEF_BENCH( return new StrokeIndirectBenchmark(
"_inflect1", SkPaint::kBevel_Join, {{0,0}, {100,0}, {0,100}, {100,100}}); )
DEF_BENCH( return new StrokeIndirectBenchmark(
"_inflect2", SkPaint::kBevel_Join, {{37,162}, {412,160}, {249,65}, {112,360}}); )
DEF_BENCH( return new StrokeIndirectBenchmark(
"_loop", SkPaint::kBevel_Join, {{0,0}, {100,0}, {0,100}, {0,0}}); )
DEF_BENCH( return new StrokeIndirectBenchmark(
"_nochop", SkPaint::kBevel_Join, {{0,0}, {50,0}, {100,50}, {100,100}}); )
DEF_BENCH( return new StrokeIndirectBenchmark(
"_quad", SkPaint::kBevel_Join, {{0,0}, {50,100}, {100,0}}); )
DEF_BENCH( return new StrokeIndirectBenchmark(
"_roundjoin", SkPaint::kRound_Join, {{0,0}, {50,100}, {100,0}}); )
class SingleVerbStrokeIndirectBenchmark : public GrStrokeIndirectTessellator::Benchmark {
public:
SingleVerbStrokeIndirectBenchmark(const char* nameSuffix, SkPathVerb verb)
: Benchmark(nameSuffix, SkPaint::kBevel_Join), fVerb(verb) {}
private:
void setupPaths(SkTArray<SkPath>* paths) override {
SkRandom rand;
for (int i = 0; i < kNumCubicsInChalkboard; ++i) {
switch (fVerb) {
case SkPathVerb::kQuad:
paths->push_back().quadTo(rand.nextF(), rand.nextF(), rand.nextF(),
rand.nextF());
break;
case SkPathVerb::kCubic:
switch (i % 3) {
case 0:
paths->push_back().cubicTo(100, 0, 0, 100, 100, 100); // 1 inflection.
break;
case 1:
paths->push_back().cubicTo(100, 0, 0, 100, 0, 0); // loop.
break;
case 2:
paths->push_back().cubicTo(50, 0, 100, 50, 100, 100); // no chop.
break;
}
break;
default:
SkUNREACHABLE;
}
}
}
const SkPathVerb fVerb;
};
DEF_BENCH( return new SingleVerbStrokeIndirectBenchmark("_singlequads", SkPathVerb::kQuad); )
DEF_BENCH( return new SingleVerbStrokeIndirectBenchmark("_singlecubics", SkPathVerb::kCubic); )