skia2/tests/StrokeIndirectTest.cpp
Michael Ludwig 4e9d5e2bdf 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-12 18:33:33 +00:00

490 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/geometry/GrWangsFormula.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"
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, GrMockOpTarget* 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);
GrStrokeTessellator::PathStrokeList pathStrokeList(path, stroke, SK_PMColor4fWHITE);
GrStrokeIndirectTessellator tessellator(GrStrokeTessellateShader::ShaderFlags::kNone,
matrix, &pathStrokeList, path.countVerbs(),
target->allocator());
tessellator.verifyResolveLevels(r, target, matrix, path, stroke);
tessellator.prepare(target, 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) {
int8_t val = *(*nextResolveLevel)++;
REPORTER_ASSERT(r, val == (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,
GrMockOpTarget* target,
const SkMatrix& viewMatrix,
const SkPath& path,
const SkStrokeRec& stroke) {
auto tolerances = GrStrokeTolerances::MakeNonHairline(viewMatrix.getMaxScale(),
stroke.getWidth());
int8_t resolveLevelForCircles = SkTPin<float>(
sk_float_nextlog2(tolerances.fNumRadialSegmentsPerRadian * SK_ScalarPI),
1, kMaxResolveLevel);
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 -resolveLevelForCircles.
if (isFirstStroke) {
firstNumSegments.push_back(-resolveLevelForCircles);
} else {
REPORTER_ASSERT(r, *nextResolveLevel++ == -resolveLevelForCircles);
}
}
float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic(
tolerances.fParametricPrecision, 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 cuspResolveLevel = (areCusps) ? resolveLevelForCircles : 0;
int signal = -((n << 4) | cuspResolveLevel);
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.fParametricPrecision, 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, GrMockOpTarget* target,
const SkMatrix& viewMatrix,
const SkStrokeRec& stroke) {
// Make sure the resolve level we assigned to each instance agrees with the actual data.
struct IndirectInstance {
SkPoint fPts[4];
SkPoint fLastControlPoint;
float fNumTotalEdges;
};
auto instance = static_cast<const IndirectInstance*>(target->peekStaticVertexData());
auto* indirect = static_cast<const GrDrawIndirectCommand*>(target->peekStaticIndirectData());
auto tolerances = GrStrokeTolerances::MakeNonHairline(viewMatrix.getMaxScale(),
stroke.getWidth());
float tolerance = test_tolerance(stroke.getJoin());
for (int i = 0; i < fChainedDrawIndirectCount; ++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;
float numParametricSegments = GrWangsFormula::cubic(tolerances.fParametricPrecision, 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;
}
}