1 /*
2 * Copyright 2020 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "tests/Test.h"
9
10 #include "include/private/SkFloatingPoint.h"
11 #include "src/core/SkGeometry.h"
12 #include "src/gpu/geometry/GrPathUtils.h"
13 #include "src/gpu/geometry/GrWangsFormula.h"
14 #include "src/gpu/mock/GrMockOpTarget.h"
15 #include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
16 #include "src/gpu/tessellate/GrStrokeTessellateShader.h"
17 #include "src/gpu/tessellate/GrTessellationPathRenderer.h"
18
make_mock_context()19 static sk_sp<GrDirectContext> make_mock_context() {
20 GrMockOptions mockOptions;
21 mockOptions.fDrawInstancedSupport = true;
22 mockOptions.fMaxTessellationSegments = 64;
23 mockOptions.fMapBufferFlags = GrCaps::kCanMap_MapFlag;
24 mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fRenderability =
25 GrMockOptions::ConfigOptions::Renderability::kMSAA;
26 mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fTexturable = true;
27 mockOptions.fIntegerSupport = true;
28
29 GrContextOptions ctxOptions;
30 ctxOptions.fGpuPathRenderers = GpuPathRenderers::kTessellation;
31
32 return GrDirectContext::MakeMock(&mockOptions, ctxOptions);
33 }
34
test_stroke(skiatest::Reporter * r,GrDirectContext * ctx,GrMockOpTarget * target,const SkPath & path,SkRandom & rand)35 static void test_stroke(skiatest::Reporter* r, GrDirectContext* ctx, GrMockOpTarget* target,
36 const SkPath& path, SkRandom& rand) {
37 SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle);
38 stroke.setStrokeStyle(.1f);
39 for (auto join : {SkPaint::kMiter_Join, SkPaint::kRound_Join}) {
40 stroke.setStrokeParams(SkPaint::kButt_Cap, join, 4);
41 for (int i = 0; i < 16; ++i) {
42 float scale = ldexpf(rand.nextF() + 1, i);
43 auto matrix = SkMatrix::Scale(scale, scale);
44 GrStrokeTessellator::PathStrokeList pathStrokeList(path, stroke, SK_PMColor4fWHITE);
45 GrStrokeIndirectTessellator tessellator(GrStrokeTessellateShader::ShaderFlags::kNone,
46 matrix, &pathStrokeList, path.countVerbs(),
47 target->allocator());
48 tessellator.verifyResolveLevels(r, target, matrix, path, stroke);
49 tessellator.prepare(target, path.countVerbs());
50 tessellator.verifyBuffers(r, target, matrix, stroke);
51 }
52 }
53 }
54
DEF_TEST(tessellate_GrStrokeIndirectTessellator,r)55 DEF_TEST(tessellate_GrStrokeIndirectTessellator, r) {
56 auto ctx = make_mock_context();
57 auto target = std::make_unique<GrMockOpTarget>(ctx);
58 SkRandom rand;
59
60 // Empty strokes.
61 SkPath path = SkPath();
62 test_stroke(r, ctx.get(), target.get(), path, rand);
63 path.moveTo(1,1);
64 test_stroke(r, ctx.get(), target.get(), path, rand);
65 path.moveTo(1,1);
66 test_stroke(r, ctx.get(), target.get(), path, rand);
67 path.close();
68 test_stroke(r, ctx.get(), target.get(), path, rand);
69 path.moveTo(1,1);
70 test_stroke(r, ctx.get(), target.get(), path, rand);
71
72 // Single line.
73 path = SkPath().lineTo(1,1);
74 test_stroke(r, ctx.get(), target.get(), path, rand);
75 path.close();
76 test_stroke(r, ctx.get(), target.get(), path, rand);
77
78 // Single quad.
79 path = SkPath().quadTo(1,0,1,1);
80 test_stroke(r, ctx.get(), target.get(), path, rand);
81 path.close();
82 test_stroke(r, ctx.get(), target.get(), path, rand);
83
84 // Single cubic.
85 path = SkPath().cubicTo(1,0,0,1,1,1);
86 test_stroke(r, ctx.get(), target.get(), path, rand);
87 path.close();
88 test_stroke(r, ctx.get(), target.get(), path, rand);
89
90 // All types of lines.
91 path.reset();
92 for (int i = 0; i < (1 << 4); ++i) {
93 path.moveTo((i>>0)&1, (i>>1)&1);
94 path.lineTo((i>>2)&1, (i>>3)&1);
95 path.close();
96 }
97 test_stroke(r, ctx.get(), target.get(), path, rand);
98
99 // All types of quads.
100 path.reset();
101 for (int i = 0; i < (1 << 6); ++i) {
102 path.moveTo((i>>0)&1, (i>>1)&1);
103 path.quadTo((i>>2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1);
104 path.close();
105 }
106 test_stroke(r, ctx.get(), target.get(), path, rand);
107
108 // All types of cubics.
109 path.reset();
110 for (int i = 0; i < (1 << 8); ++i) {
111 path.moveTo((i>>0)&1, (i>>1)&1);
112 path.cubicTo((i>>2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1, (i>>6)&1, (i>>7)&1);
113 path.close();
114 }
115 test_stroke(r, ctx.get(), target.get(), path, rand);
116
117 {
118 // This cubic has a convex-180 chop at T=1-"epsilon"
119 static const uint32_t hexPts[] = {0x3ee0ac74, 0x3f1e061a, 0x3e0fc408, 0x3f457230,
120 0x3f42ac7c, 0x3f70d76c, 0x3f4e6520, 0x3f6acafa};
121 SkPoint pts[4];
122 memcpy(pts, hexPts, sizeof(pts));
123 test_stroke(r, ctx.get(), target.get(),
124 SkPath().moveTo(pts[0]).cubicTo(pts[1], pts[2], pts[3]).close(), rand);
125 }
126
127 // Random paths.
128 for (int j = 0; j < 50; ++j) {
129 path.reset();
130 // Empty contours behave differently if closed.
131 path.moveTo(0,0);
132 path.moveTo(0,0);
133 path.close();
134 path.moveTo(0,0);
135 SkPoint startPoint = {rand.nextF(), rand.nextF()};
136 path.moveTo(startPoint);
137 // Degenerate curves get skipped.
138 path.lineTo(startPoint);
139 path.quadTo(startPoint, startPoint);
140 path.cubicTo(startPoint, startPoint, startPoint);
141 for (int i = 0; i < 100; ++i) {
142 switch (rand.nextRangeU(0, 4)) {
143 case 0:
144 path.lineTo(rand.nextF(), rand.nextF());
145 break;
146 case 1:
147 path.quadTo(rand.nextF(), rand.nextF(), rand.nextF(), rand.nextF());
148 break;
149 case 2:
150 case 3:
151 case 4:
152 path.cubicTo(rand.nextF(), rand.nextF(), rand.nextF(), rand.nextF(),
153 rand.nextF(), rand.nextF());
154 break;
155 default:
156 SkUNREACHABLE;
157 }
158 if (i % 19 == 0) {
159 switch (i/19 % 4) {
160 case 0:
161 break;
162 case 1:
163 path.lineTo(startPoint);
164 break;
165 case 2:
166 path.quadTo(SkPoint::Make(1.1f, 1.1f), startPoint);
167 break;
168 case 3:
169 path.cubicTo(SkPoint::Make(1.1f, 1.1f), SkPoint::Make(1.1f, 1.1f),
170 startPoint);
171 break;
172 }
173 path.close();
174 if (rand.nextU() & 1) { // Implicit or explicit move?
175 startPoint = {rand.nextF(), rand.nextF()};
176 path.moveTo(startPoint);
177 }
178 }
179 }
180 test_stroke(r, ctx.get(), target.get(), path, rand);
181 }
182 }
183
184 // Returns the control point for the first/final join of a contour.
185 // If the contour is not closed, returns the start point.
get_contour_closing_control_point(SkPathPriv::RangeIter iter,const SkPathPriv::RangeIter & end)186 static SkPoint get_contour_closing_control_point(SkPathPriv::RangeIter iter,
187 const SkPathPriv::RangeIter& end) {
188 auto [verb, p, w] = *iter;
189 SkASSERT(verb == SkPathVerb::kMove);
190 // Peek ahead to find the last control point.
191 SkPoint startPoint=p[0], lastControlPoint=p[0];
192 for (++iter; iter != end; ++iter) {
193 auto [verb, p, w] = *iter;
194 switch (verb) {
195 case SkPathVerb::kMove:
196 return startPoint;
197 case SkPathVerb::kCubic:
198 if (p[2] != p[3]) {
199 lastControlPoint = p[2];
200 break;
201 }
202 [[fallthrough]];
203 case SkPathVerb::kQuad:
204 if (p[1] != p[2]) {
205 lastControlPoint = p[1];
206 break;
207 }
208 [[fallthrough]];
209 case SkPathVerb::kLine:
210 if (p[0] != p[1]) {
211 lastControlPoint = p[0];
212 }
213 break;
214 case SkPathVerb::kConic:
215 SkUNREACHABLE;
216 case SkPathVerb::kClose:
217 return (p[0] == startPoint) ? lastControlPoint : p[0];
218 }
219 }
220 return startPoint;
221 }
222
check_resolve_level(skiatest::Reporter * r,float numCombinedSegments,int8_t actualLevel,float tolerance,bool printError=true)223 static bool check_resolve_level(skiatest::Reporter* r, float numCombinedSegments,
224 int8_t actualLevel, float tolerance, bool printError = true) {
225 int8_t expectedLevel = sk_float_nextlog2(numCombinedSegments);
226 if ((actualLevel > expectedLevel &&
227 actualLevel > sk_float_nextlog2(numCombinedSegments + tolerance)) ||
228 (actualLevel < expectedLevel &&
229 actualLevel < sk_float_nextlog2(numCombinedSegments - tolerance))) {
230 if (printError) {
231 ERRORF(r, "expected %f segments => resolveLevel=%i (got %i)\n",
232 numCombinedSegments, expectedLevel, actualLevel);
233 }
234 return false;
235 }
236 return true;
237 }
238
check_first_resolve_levels(skiatest::Reporter * r,const SkTArray<float> & firstNumSegments,int8_t ** nextResolveLevel,float tolerance)239 static bool check_first_resolve_levels(skiatest::Reporter* r,
240 const SkTArray<float>& firstNumSegments,
241 int8_t** nextResolveLevel, float tolerance) {
242 for (float numSegments : firstNumSegments) {
243 if (numSegments < 0) {
244 int8_t val = *(*nextResolveLevel)++;
245 REPORTER_ASSERT(r, val == (int)numSegments);
246 continue;
247 }
248 // The first stroke's resolve levels aren't written out until the end of
249 // the contour.
250 if (!check_resolve_level(r, numSegments, *(*nextResolveLevel)++, tolerance)) {
251 return false;
252 }
253 }
254 return true;
255 }
256
test_tolerance(SkPaint::Join joinType)257 static float test_tolerance(SkPaint::Join joinType) {
258 // Ensure our fast approximation falls within 1.15 tessellation segments of the "correct"
259 // answer. This is more than good enough when our matrix scale can go up to 2^17.
260 float tolerance = 1.15f;
261 if (joinType == SkPaint::kRound_Join) {
262 // We approximate two different angles when there are round joins. Double the tolerance.
263 tolerance *= 2;
264 }
265 return tolerance;
266 }
267
verifyResolveLevels(skiatest::Reporter * r,GrMockOpTarget * target,const SkMatrix & viewMatrix,const SkPath & path,const SkStrokeRec & stroke)268 void GrStrokeIndirectTessellator::verifyResolveLevels(skiatest::Reporter* r,
269 GrMockOpTarget* target,
270 const SkMatrix& viewMatrix,
271 const SkPath& path,
272 const SkStrokeRec& stroke) {
273 auto tolerances = GrStrokeTolerances::MakeNonHairline(viewMatrix.getMaxScale(),
274 stroke.getWidth());
275 int8_t resolveLevelForCircles = SkTPin<float>(
276 sk_float_nextlog2(tolerances.fNumRadialSegmentsPerRadian * SK_ScalarPI),
277 1, kMaxResolveLevel);
278 float tolerance = test_tolerance(stroke.getJoin());
279 int8_t* nextResolveLevel = fResolveLevels;
280 auto iterate = SkPathPriv::Iterate(path);
281 SkSTArray<3, float> firstNumSegments;
282 bool isFirstStroke = true;
283 SkPoint startPoint = {0,0};
284 SkPoint lastControlPoint;
285 for (auto iter = iterate.begin(); iter != iterate.end(); ++iter) {
286 auto [verb, pts, w] = *iter;
287 switch (verb) {
288 int n;
289 SkPoint chops[10];
290 case SkPathVerb::kMove:
291 startPoint = pts[0];
292 lastControlPoint = get_contour_closing_control_point(iter, iterate.end());
293 if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
294 tolerance)) {
295 return;
296 }
297 firstNumSegments.reset();
298 isFirstStroke = true;
299 break;
300 case SkPathVerb::kLine:
301 if (pts[0] == pts[1]) {
302 break;
303 }
304 if (stroke.getJoin() == SkPaint::kRound_Join) {
305 float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
306 pts[1] - pts[0]);
307 float numSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
308 if (isFirstStroke) {
309 firstNumSegments.push_back(numSegments);
310 } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
311 tolerance)) {
312 return;
313 }
314 }
315 lastControlPoint = pts[0];
316 isFirstStroke = false;
317 break;
318 case SkPathVerb::kQuad: {
319 if (pts[0] == pts[1] && pts[1] == pts[2]) {
320 break;
321 }
322 SkVector a = pts[1] - pts[0];
323 SkVector b = pts[2] - pts[1];
324 bool hasCusp = (a.cross(b) == 0 && a.dot(b) < 0);
325 if (hasCusp) {
326 // The quad has a cusp. Make sure we wrote out a -resolveLevelForCircles.
327 if (isFirstStroke) {
328 firstNumSegments.push_back(-resolveLevelForCircles);
329 } else {
330 REPORTER_ASSERT(r, *nextResolveLevel++ == -resolveLevelForCircles);
331 }
332 }
333 float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic(
334 tolerances.fParametricPrecision, pts);
335 float rotation = (hasCusp) ? 0 : SkMeasureQuadRotation(pts);
336 if (stroke.getJoin() == SkPaint::kRound_Join) {
337 SkVector controlPoint = (pts[0] == pts[1]) ? pts[2] : pts[1];
338 rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
339 controlPoint - pts[0]);
340 }
341 float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
342 float numSegments = numParametricSegments + numRadialSegments;
343 if (!hasCusp || stroke.getJoin() == SkPaint::kRound_Join) {
344 if (isFirstStroke) {
345 firstNumSegments.push_back(numSegments);
346 } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
347 tolerance)) {
348 return;
349 }
350 }
351 lastControlPoint = (pts[2] == pts[1]) ? pts[0] : pts[1];
352 isFirstStroke = false;
353 break;
354 }
355 case SkPathVerb::kCubic: {
356 if (pts[0] == pts[1] && pts[1] == pts[2] && pts[2] == pts[3]) {
357 break;
358 }
359 float T[2];
360 bool areCusps = false;
361 n = GrPathUtils::findCubicConvex180Chops(pts, T, &areCusps);
362 SkChopCubicAt(pts, chops, T, n);
363 if (n > 0) {
364 int cuspResolveLevel = (areCusps) ? resolveLevelForCircles : 0;
365 int signal = -((n << 4) | cuspResolveLevel);
366 if (isFirstStroke) {
367 firstNumSegments.push_back((float)signal);
368 } else {
369 REPORTER_ASSERT(r, *nextResolveLevel++ == signal);
370 }
371 }
372 for (int i = 0; i <= n; ++i) {
373 // Find the number of segments with our unoptimized approach and make sure
374 // it matches the answer we got already.
375 SkPoint* p = chops + i*3;
376 float numParametricSegments =
377 GrWangsFormula::cubic(tolerances.fParametricPrecision, p);
378 SkVector tan0 =
379 ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0];
380 SkVector tan1 =
381 p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]);
382 float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
383 if (i == 0 && stroke.getJoin() == SkPaint::kRound_Join) {
384 rotation += SkMeasureAngleBetweenVectors(p[0] - lastControlPoint, tan0);
385 }
386 float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
387 float numSegments = numParametricSegments + numRadialSegments;
388 if (isFirstStroke) {
389 firstNumSegments.push_back(numSegments);
390 } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
391 tolerance)) {
392 return;
393 }
394 }
395 lastControlPoint =
396 (pts[3] == pts[2]) ? (pts[2] == pts[1]) ? pts[0] : pts[1] : pts[2];
397 isFirstStroke = false;
398 break;
399 }
400 case SkPathVerb::kConic:
401 SkUNREACHABLE;
402 case SkPathVerb::kClose:
403 if (pts[0] != startPoint) {
404 SkASSERT(!isFirstStroke);
405 if (stroke.getJoin() == SkPaint::kRound_Join) {
406 // Line from pts[0] to startPoint, with a preceding join.
407 float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
408 startPoint - pts[0]);
409 if (!check_resolve_level(
410 r, rotation * tolerances.fNumRadialSegmentsPerRadian,
411 *nextResolveLevel++, tolerance)) {
412 return;
413 }
414 }
415 }
416 if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
417 tolerance)) {
418 return;
419 }
420 firstNumSegments.reset();
421 isFirstStroke = true;
422 break;
423 }
424 }
425 if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, tolerance)) {
426 return;
427 }
428 firstNumSegments.reset();
429 SkASSERT(nextResolveLevel == fResolveLevels + fResolveLevelArrayCount);
430 }
431
verifyBuffers(skiatest::Reporter * r,GrMockOpTarget * target,const SkMatrix & viewMatrix,const SkStrokeRec & stroke)432 void GrStrokeIndirectTessellator::verifyBuffers(skiatest::Reporter* r, GrMockOpTarget* target,
433 const SkMatrix& viewMatrix,
434 const SkStrokeRec& stroke) {
435 // Make sure the resolve level we assigned to each instance agrees with the actual data.
436 struct IndirectInstance {
437 SkPoint fPts[4];
438 SkPoint fLastControlPoint;
439 float fNumTotalEdges;
440 };
441 auto instance = static_cast<const IndirectInstance*>(target->peekStaticVertexData());
442 auto* indirect = static_cast<const GrDrawIndirectCommand*>(target->peekStaticIndirectData());
443 auto tolerances = GrStrokeTolerances::MakeNonHairline(viewMatrix.getMaxScale(),
444 stroke.getWidth());
445 float tolerance = test_tolerance(stroke.getJoin());
446 for (int i = 0; i < fChainedDrawIndirectCount; ++i) {
447 int numExtraEdgesInJoin = (stroke.getJoin() == SkPaint::kMiter_Join) ? 4 : 3;
448 int numStrokeEdges = indirect->fVertexCount/2 - numExtraEdgesInJoin;
449 int numSegments = numStrokeEdges - 1;
450 bool isPow2 = !(numSegments & (numSegments - 1));
451 REPORTER_ASSERT(r, isPow2);
452 int resolveLevel = sk_float_nextlog2(numSegments);
453 REPORTER_ASSERT(r, 1 << resolveLevel == numSegments);
454 for (unsigned j = 0; j < indirect->fInstanceCount; ++j) {
455 SkASSERT(fabsf(instance->fNumTotalEdges) == indirect->fVertexCount/2);
456 const SkPoint* p = instance->fPts;
457 float numParametricSegments = GrWangsFormula::cubic(tolerances.fParametricPrecision, p);
458 float alternateNumParametricSegments = numParametricSegments;
459 if (p[0] == p[1] && p[2] == p[3]) {
460 // We articulate lines as "p0,p0,p1,p1". This one might actually expect 0 parametric
461 // segments.
462 alternateNumParametricSegments = 0;
463 }
464 SkVector tan0 = ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0];
465 SkVector tan1 = p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]);
466 float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
467 // Negative fNumTotalEdges means the curve is a chop, and chops always get treated as a
468 // bevel join.
469 if (stroke.getJoin() == SkPaint::kRound_Join && instance->fNumTotalEdges > 0) {
470 SkVector lastTangent = p[0] - instance->fLastControlPoint;
471 rotation += SkMeasureAngleBetweenVectors(lastTangent, tan0);
472 }
473 // Degenerate strokes are a special case that actually mean the GPU should draw a cusp
474 // (i.e. circle).
475 if (p[0] == p[1] && p[1] == p[2] && p[2] == p[3]) {
476 rotation = SK_ScalarPI;
477 }
478 float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
479 float numSegments = numParametricSegments + numRadialSegments;
480 float alternateNumSegments = alternateNumParametricSegments + numRadialSegments;
481 if (!check_resolve_level(r, numSegments, resolveLevel, tolerance, false) &&
482 !check_resolve_level(r, alternateNumSegments, resolveLevel, tolerance, true)) {
483 return;
484 }
485 ++instance;
486 }
487 ++indirect;
488 }
489 }
490