skia2/tests/StrokeIndirectTest.cpp
Chris Dalton 39ce12e0d4 Don't store tessellation tolerances on GrStrokeOp
Instead we calculate these values on the fly as needed. This is
necessary for hairlines because the tessellator will operate on them
in post-transform space, which requires different tolerances than the
setup code.

Bug: skia:10419
Change-Id: Ia8ffa8858b45949521c085ccbe5712b3842f785f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/343499
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
2020-12-14 18:16:47 +00:00

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/GrStrokeIndirectOp.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 op = GrOp::Make<GrStrokeIndirectOp>(ctx, GrAAType::kMSAA,
SkMatrix::Scale(scale, scale), path, stroke,
GrPaint());
auto strokeIndirectOp = op->cast<GrStrokeIndirectOp>();
strokeIndirectOp->verifyPrePrepareResolveLevels(r, target);
strokeIndirectOp->verifyPrepareBuffers(r, target);
}
}
}
DEF_TEST(tessellate_GrStrokeIndirectOp, 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 GrStrokeIndirectOp::verifyPrePrepareResolveLevels(skiatest::Reporter* r,
GrMeshDrawOp::Target* target) {
GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
float tolerance = test_tolerance(fStroke.getJoin());
// Fill in fResolveLevels with our resolve levels for each curve.
this->prePrepareResolveLevels(target->allocator());
int8_t* nextResolveLevel = fResolveLevels;
// Now check out answers.
for (const SkPath& path : fPathList) {
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 (fStroke.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 (fStroke.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 || fStroke.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 && fStroke.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 (fStroke.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 GrStrokeIndirectOp::verifyPrepareBuffers(skiatest::Reporter* r, GrMeshDrawOp::Target* target) {
using IndirectInstance = GrStrokeTessellateShader::IndirectInstance;
GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
float tolerance = test_tolerance(fStroke.getJoin());
// Make sure the resolve level we assign to each instance agrees with the actual data.
this->prepareBuffers(target);
// 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 = (fStroke.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 (fStroke.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;
}
}