2224100b25
Now there is only one op to tessellate a stroke, and it creates its own GrStrokeIndirectTessellator or GrStrokeHardwareTessellator internally. This will allow us to dynamically switch into hardware tessellation when we need to batch strokes that have different parameters or colors. Bug: chromium:1172543 Bug: skia:10419 Change-Id: I3cddb855fdbb9ab018785584497c843e3e31b75e Reviewed-on: https://skia-review.googlesource.com/c/skia/+/366056 Commit-Queue: Chris Dalton <csmartdalton@google.com> Reviewed-by: Greg Daniel <egdaniel@google.com>
482 lines
21 KiB
C++
482 lines
21 KiB
C++
/*
|
|
* 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 "tests/Test.h"
|
|
|
|
#include "include/private/SkFloatingPoint.h"
|
|
#include "src/core/SkGeometry.h"
|
|
#include "src/gpu/geometry/GrPathUtils.h"
|
|
#include "src/gpu/mock/GrMockOpTarget.h"
|
|
#include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
|
|
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
|
|
#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
|
|
#include "src/gpu/tessellate/GrWangsFormula.h"
|
|
|
|
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;
|
|
|
|
return GrDirectContext::MakeMock(&mockOptions, ctxOptions);
|
|
}
|
|
|
|
static void test_stroke(skiatest::Reporter* r, GrDirectContext* ctx, GrMeshDrawOp::Target* target,
|
|
const SkPath& path, SkRandom& rand) {
|
|
SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle);
|
|
stroke.setStrokeStyle(.1f);
|
|
for (auto join : {SkPaint::kMiter_Join, SkPaint::kRound_Join}) {
|
|
stroke.setStrokeParams(SkPaint::kButt_Cap, join, 4);
|
|
for (int i = 0; i < 16; ++i) {
|
|
float scale = ldexpf(rand.nextF() + 1, i);
|
|
auto matrix = SkMatrix::Scale(scale, scale);
|
|
GrStrokeIndirectTessellator tessellator(matrix, path, stroke, path.countVerbs(),
|
|
target->allocator());
|
|
tessellator.verifyResolveLevels(r, target, matrix, path, stroke);
|
|
tessellator.prepare(target, matrix, path, stroke, path.countVerbs());
|
|
tessellator.verifyBuffers(r, target, matrix, stroke);
|
|
}
|
|
}
|
|
}
|
|
|
|
DEF_TEST(tessellate_GrStrokeIndirectTessellator, r) {
|
|
auto ctx = make_mock_context();
|
|
auto target = std::make_unique<GrMockOpTarget>(ctx);
|
|
SkRandom rand;
|
|
|
|
// Empty strokes.
|
|
SkPath path = SkPath();
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.moveTo(1,1);
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.moveTo(1,1);
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.close();
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.moveTo(1,1);
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
// Single line.
|
|
path = SkPath().lineTo(1,1);
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.close();
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
// Single quad.
|
|
path = SkPath().quadTo(1,0,1,1);
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.close();
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
// Single cubic.
|
|
path = SkPath().cubicTo(1,0,0,1,1,1);
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
path.close();
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
// All types of lines.
|
|
path.reset();
|
|
for (int i = 0; i < (1 << 4); ++i) {
|
|
path.moveTo((i>>0)&1, (i>>1)&1);
|
|
path.lineTo((i>>2)&1, (i>>3)&1);
|
|
path.close();
|
|
}
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
// All types of quads.
|
|
path.reset();
|
|
for (int i = 0; i < (1 << 6); ++i) {
|
|
path.moveTo((i>>0)&1, (i>>1)&1);
|
|
path.quadTo((i>>2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1);
|
|
path.close();
|
|
}
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
// All types of cubics.
|
|
path.reset();
|
|
for (int i = 0; i < (1 << 8); ++i) {
|
|
path.moveTo((i>>0)&1, (i>>1)&1);
|
|
path.cubicTo((i>>2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1, (i>>6)&1, (i>>7)&1);
|
|
path.close();
|
|
}
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
|
|
{
|
|
// This cubic has a convex-180 chop at T=1-"epsilon"
|
|
static const uint32_t hexPts[] = {0x3ee0ac74, 0x3f1e061a, 0x3e0fc408, 0x3f457230,
|
|
0x3f42ac7c, 0x3f70d76c, 0x3f4e6520, 0x3f6acafa};
|
|
SkPoint pts[4];
|
|
memcpy(pts, hexPts, sizeof(pts));
|
|
test_stroke(r, ctx.get(), target.get(),
|
|
SkPath().moveTo(pts[0]).cubicTo(pts[1], pts[2], pts[3]).close(), rand);
|
|
}
|
|
|
|
// Random paths.
|
|
for (int j = 0; j < 50; ++j) {
|
|
path.reset();
|
|
// Empty contours behave differently if closed.
|
|
path.moveTo(0,0);
|
|
path.moveTo(0,0);
|
|
path.close();
|
|
path.moveTo(0,0);
|
|
SkPoint startPoint = {rand.nextF(), rand.nextF()};
|
|
path.moveTo(startPoint);
|
|
// Degenerate curves get skipped.
|
|
path.lineTo(startPoint);
|
|
path.quadTo(startPoint, startPoint);
|
|
path.cubicTo(startPoint, startPoint, startPoint);
|
|
for (int i = 0; i < 100; ++i) {
|
|
switch (rand.nextRangeU(0, 4)) {
|
|
case 0:
|
|
path.lineTo(rand.nextF(), rand.nextF());
|
|
break;
|
|
case 1:
|
|
path.quadTo(rand.nextF(), rand.nextF(), rand.nextF(), rand.nextF());
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
case 4:
|
|
path.cubicTo(rand.nextF(), rand.nextF(), rand.nextF(), rand.nextF(),
|
|
rand.nextF(), rand.nextF());
|
|
break;
|
|
default:
|
|
SkUNREACHABLE;
|
|
}
|
|
if (i % 19 == 0) {
|
|
switch (i/19 % 4) {
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
path.lineTo(startPoint);
|
|
break;
|
|
case 2:
|
|
path.quadTo(SkPoint::Make(1.1f, 1.1f), startPoint);
|
|
break;
|
|
case 3:
|
|
path.cubicTo(SkPoint::Make(1.1f, 1.1f), SkPoint::Make(1.1f, 1.1f),
|
|
startPoint);
|
|
break;
|
|
}
|
|
path.close();
|
|
if (rand.nextU() & 1) { // Implicit or explicit move?
|
|
startPoint = {rand.nextF(), rand.nextF()};
|
|
path.moveTo(startPoint);
|
|
}
|
|
}
|
|
}
|
|
test_stroke(r, ctx.get(), target.get(), path, rand);
|
|
}
|
|
}
|
|
|
|
// Returns the control point for the first/final join of a contour.
|
|
// If the contour is not closed, returns the start point.
|
|
static SkPoint get_contour_closing_control_point(SkPathPriv::RangeIter iter,
|
|
const SkPathPriv::RangeIter& end) {
|
|
auto [verb, p, w] = *iter;
|
|
SkASSERT(verb == SkPathVerb::kMove);
|
|
// Peek ahead to find the last control point.
|
|
SkPoint startPoint=p[0], lastControlPoint=p[0];
|
|
for (++iter; iter != end; ++iter) {
|
|
auto [verb, p, w] = *iter;
|
|
switch (verb) {
|
|
case SkPathVerb::kMove:
|
|
return startPoint;
|
|
case SkPathVerb::kCubic:
|
|
if (p[2] != p[3]) {
|
|
lastControlPoint = p[2];
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case SkPathVerb::kQuad:
|
|
if (p[1] != p[2]) {
|
|
lastControlPoint = p[1];
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case SkPathVerb::kLine:
|
|
if (p[0] != p[1]) {
|
|
lastControlPoint = p[0];
|
|
}
|
|
break;
|
|
case SkPathVerb::kConic:
|
|
SkUNREACHABLE;
|
|
case SkPathVerb::kClose:
|
|
return (p[0] == startPoint) ? lastControlPoint : p[0];
|
|
}
|
|
}
|
|
return startPoint;
|
|
}
|
|
|
|
static bool check_resolve_level(skiatest::Reporter* r, float numCombinedSegments,
|
|
int8_t actualLevel, float tolerance, bool printError = true) {
|
|
int8_t expectedLevel = sk_float_nextlog2(numCombinedSegments);
|
|
if ((actualLevel > expectedLevel &&
|
|
actualLevel > sk_float_nextlog2(numCombinedSegments + tolerance)) ||
|
|
(actualLevel < expectedLevel &&
|
|
actualLevel < sk_float_nextlog2(numCombinedSegments - tolerance))) {
|
|
if (printError) {
|
|
ERRORF(r, "expected %f segments => resolveLevel=%i (got %i)\n",
|
|
numCombinedSegments, expectedLevel, actualLevel);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool check_first_resolve_levels(skiatest::Reporter* r,
|
|
const SkTArray<float>& firstNumSegments,
|
|
int8_t** nextResolveLevel, float tolerance) {
|
|
for (float numSegments : firstNumSegments) {
|
|
if (numSegments < 0) {
|
|
REPORTER_ASSERT(r, *(*nextResolveLevel)++ == (int)numSegments);
|
|
continue;
|
|
}
|
|
// The first stroke's resolve levels aren't written out until the end of
|
|
// the contour.
|
|
if (!check_resolve_level(r, numSegments, *(*nextResolveLevel)++, tolerance)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static float test_tolerance(SkPaint::Join joinType) {
|
|
// Ensure our fast approximation falls within 1.15 tessellation segments of the "correct"
|
|
// answer. This is more than good enough when our matrix scale can go up to 2^17.
|
|
float tolerance = 1.15f;
|
|
if (joinType == SkPaint::kRound_Join) {
|
|
// We approximate two different angles when there are round joins. Double the tolerance.
|
|
tolerance *= 2;
|
|
}
|
|
return tolerance;
|
|
}
|
|
|
|
void GrStrokeIndirectTessellator::verifyResolveLevels(skiatest::Reporter* r,
|
|
GrMeshDrawOp::Target* target,
|
|
const SkMatrix& viewMatrix,
|
|
const SkPath& path,
|
|
const SkStrokeRec& stroke) {
|
|
GrStrokeTessellateShader::Tolerances tolerances(viewMatrix.getMaxScale(), stroke.getWidth());
|
|
float tolerance = test_tolerance(stroke.getJoin());
|
|
int8_t* nextResolveLevel = fResolveLevels;
|
|
auto iterate = SkPathPriv::Iterate(path);
|
|
SkSTArray<3, float> firstNumSegments;
|
|
bool isFirstStroke = true;
|
|
SkPoint startPoint = {0,0};
|
|
SkPoint lastControlPoint;
|
|
for (auto iter = iterate.begin(); iter != iterate.end(); ++iter) {
|
|
auto [verb, pts, w] = *iter;
|
|
switch (verb) {
|
|
int n;
|
|
SkPoint chops[10];
|
|
case SkPathVerb::kMove:
|
|
startPoint = pts[0];
|
|
lastControlPoint = get_contour_closing_control_point(iter, iterate.end());
|
|
if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
|
|
tolerance)) {
|
|
return;
|
|
}
|
|
firstNumSegments.reset();
|
|
isFirstStroke = true;
|
|
break;
|
|
case SkPathVerb::kLine:
|
|
if (pts[0] == pts[1]) {
|
|
break;
|
|
}
|
|
if (stroke.getJoin() == SkPaint::kRound_Join) {
|
|
float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
|
|
pts[1] - pts[0]);
|
|
float numSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
|
|
if (isFirstStroke) {
|
|
firstNumSegments.push_back(numSegments);
|
|
} else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
|
|
tolerance)) {
|
|
return;
|
|
}
|
|
}
|
|
lastControlPoint = pts[0];
|
|
isFirstStroke = false;
|
|
break;
|
|
case SkPathVerb::kQuad: {
|
|
if (pts[0] == pts[1] && pts[1] == pts[2]) {
|
|
break;
|
|
}
|
|
SkVector a = pts[1] - pts[0];
|
|
SkVector b = pts[2] - pts[1];
|
|
bool hasCusp = (a.cross(b) == 0 && a.dot(b) < 0);
|
|
if (hasCusp) {
|
|
// The quad has a cusp. Make sure we wrote out a -1 to signal that.
|
|
if (isFirstStroke) {
|
|
firstNumSegments.push_back(-1);
|
|
} else {
|
|
REPORTER_ASSERT(r, *nextResolveLevel++ == -1);
|
|
}
|
|
}
|
|
float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic(
|
|
tolerances.fParametricIntolerance, pts);
|
|
float rotation = (hasCusp) ? 0 : SkMeasureQuadRotation(pts);
|
|
if (stroke.getJoin() == SkPaint::kRound_Join) {
|
|
SkVector controlPoint = (pts[0] == pts[1]) ? pts[2] : pts[1];
|
|
rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
|
|
controlPoint - pts[0]);
|
|
}
|
|
float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
|
|
float numSegments = numParametricSegments + numRadialSegments;
|
|
if (!hasCusp || stroke.getJoin() == SkPaint::kRound_Join) {
|
|
if (isFirstStroke) {
|
|
firstNumSegments.push_back(numSegments);
|
|
} else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
|
|
tolerance)) {
|
|
return;
|
|
}
|
|
}
|
|
lastControlPoint = (pts[2] == pts[1]) ? pts[0] : pts[1];
|
|
isFirstStroke = false;
|
|
break;
|
|
}
|
|
case SkPathVerb::kCubic: {
|
|
if (pts[0] == pts[1] && pts[1] == pts[2] && pts[2] == pts[3]) {
|
|
break;
|
|
}
|
|
float T[2];
|
|
bool areCusps = false;
|
|
n = GrPathUtils::findCubicConvex180Chops(pts, T, &areCusps);
|
|
SkChopCubicAt(pts, chops, T, n);
|
|
if (n > 0) {
|
|
int signal = -((n << 1) | (int)areCusps);
|
|
if (isFirstStroke) {
|
|
firstNumSegments.push_back((float)signal);
|
|
} else {
|
|
REPORTER_ASSERT(r, *nextResolveLevel++ == signal);
|
|
}
|
|
}
|
|
for (int i = 0; i <= n; ++i) {
|
|
// Find the number of segments with our unoptimized approach and make sure
|
|
// it matches the answer we got already.
|
|
SkPoint* p = chops + i*3;
|
|
float numParametricSegments =
|
|
GrWangsFormula::cubic(tolerances.fParametricIntolerance, p);
|
|
SkVector tan0 =
|
|
((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0];
|
|
SkVector tan1 =
|
|
p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]);
|
|
float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
|
|
if (i == 0 && stroke.getJoin() == SkPaint::kRound_Join) {
|
|
rotation += SkMeasureAngleBetweenVectors(p[0] - lastControlPoint, tan0);
|
|
}
|
|
float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
|
|
float numSegments = numParametricSegments + numRadialSegments;
|
|
if (isFirstStroke) {
|
|
firstNumSegments.push_back(numSegments);
|
|
} else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
|
|
tolerance)) {
|
|
return;
|
|
}
|
|
}
|
|
lastControlPoint =
|
|
(pts[3] == pts[2]) ? (pts[2] == pts[1]) ? pts[0] : pts[1] : pts[2];
|
|
isFirstStroke = false;
|
|
break;
|
|
}
|
|
case SkPathVerb::kConic:
|
|
SkUNREACHABLE;
|
|
case SkPathVerb::kClose:
|
|
if (pts[0] != startPoint) {
|
|
SkASSERT(!isFirstStroke);
|
|
if (stroke.getJoin() == SkPaint::kRound_Join) {
|
|
// Line from pts[0] to startPoint, with a preceding join.
|
|
float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
|
|
startPoint - pts[0]);
|
|
if (!check_resolve_level(
|
|
r, rotation * tolerances.fNumRadialSegmentsPerRadian,
|
|
*nextResolveLevel++, tolerance)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
|
|
tolerance)) {
|
|
return;
|
|
}
|
|
firstNumSegments.reset();
|
|
isFirstStroke = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, tolerance)) {
|
|
return;
|
|
}
|
|
firstNumSegments.reset();
|
|
SkASSERT(nextResolveLevel == fResolveLevels + fResolveLevelArrayCount);
|
|
}
|
|
|
|
void GrStrokeIndirectTessellator::verifyBuffers(skiatest::Reporter* r,
|
|
GrMeshDrawOp::Target* target,
|
|
const SkMatrix& viewMatrix,
|
|
const SkStrokeRec& stroke) {
|
|
using IndirectInstance = GrStrokeTessellateShader::IndirectInstance;
|
|
GrStrokeTessellateShader::Tolerances tolerances(viewMatrix.getMaxScale(), stroke.getWidth());
|
|
float tolerance = test_tolerance(stroke.getJoin());
|
|
// Make sure the resolve level we assign to each instance agrees with the actual data.
|
|
// GrMockOpTarget returns the same pointers every time.
|
|
int _;
|
|
auto instance = (const IndirectInstance*)target->makeVertexSpace(0, 0, nullptr, &_);
|
|
size_t __;
|
|
auto indirect = target->makeDrawIndirectSpace(0, nullptr, &__);
|
|
for (int i = 0; i < fDrawIndirectCount; ++i) {
|
|
int numExtraEdgesInJoin = (stroke.getJoin() == SkPaint::kMiter_Join) ? 4 : 3;
|
|
int numStrokeEdges = indirect->fVertexCount/2 - numExtraEdgesInJoin;
|
|
int numSegments = numStrokeEdges - 1;
|
|
bool isPow2 = !(numSegments & (numSegments - 1));
|
|
REPORTER_ASSERT(r, isPow2);
|
|
int resolveLevel = sk_float_nextlog2(numSegments);
|
|
REPORTER_ASSERT(r, 1 << resolveLevel == numSegments);
|
|
for (unsigned j = 0; j < indirect->fInstanceCount; ++j) {
|
|
SkASSERT(fabsf(instance->fNumTotalEdges) == indirect->fVertexCount/2);
|
|
const SkPoint* p = instance->fPts.data();
|
|
float numParametricSegments = GrWangsFormula::cubic(
|
|
tolerances.fParametricIntolerance, p);
|
|
float alternateNumParametricSegments = numParametricSegments;
|
|
if (p[0] == p[1] && p[2] == p[3]) {
|
|
// We articulate lines as "p0,p0,p1,p1". This one might actually expect 0 parametric
|
|
// segments.
|
|
alternateNumParametricSegments = 0;
|
|
}
|
|
SkVector tan0 = ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0];
|
|
SkVector tan1 = p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]);
|
|
float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
|
|
// Negative fNumTotalEdges means the curve is a chop, and chops always get treated as a
|
|
// bevel join.
|
|
if (stroke.getJoin() == SkPaint::kRound_Join && instance->fNumTotalEdges > 0) {
|
|
SkVector lastTangent = p[0] - instance->fLastControlPoint;
|
|
rotation += SkMeasureAngleBetweenVectors(lastTangent, tan0);
|
|
}
|
|
// Degenerate strokes are a special case that actually mean the GPU should draw a cusp
|
|
// (i.e. circle).
|
|
if (p[0] == p[1] && p[1] == p[2] && p[2] == p[3]) {
|
|
rotation = SK_ScalarPI;
|
|
}
|
|
float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
|
|
float numSegments = numParametricSegments + numRadialSegments;
|
|
float alternateNumSegments = alternateNumParametricSegments + numRadialSegments;
|
|
if (!check_resolve_level(r, numSegments, resolveLevel, tolerance, false) &&
|
|
!check_resolve_level(r, alternateNumSegments, resolveLevel, tolerance, true)) {
|
|
return;
|
|
}
|
|
++instance;
|
|
}
|
|
++indirect;
|
|
}
|
|
}
|