1 /*
2 * Copyright 2019 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 "samplecode/Sample.h"
9
10 #include "src/gpu/geometry/GrQuad.h"
11 #include "src/gpu/ops/QuadPerEdgeAA.h"
12
13 #include "include/core/SkCanvas.h"
14 #include "include/core/SkPaint.h"
15 #include "include/effects/SkDashPathEffect.h"
16 #include "include/pathops/SkPathOps.h"
17 #include "include/private/SkTPin.h"
18
19 using VertexSpec = skgpu::v1::QuadPerEdgeAA::VertexSpec;
20 using ColorType = skgpu::v1::QuadPerEdgeAA::ColorType;
21 using Subset = skgpu::v1::QuadPerEdgeAA::Subset;
22 using IndexBufferOption = skgpu::v1::QuadPerEdgeAA::IndexBufferOption;
23
24 // Draw a line through the two points, outset by a fixed length in screen space
draw_extended_line(SkCanvas * canvas,const SkPaint paint,const SkPoint & p0,const SkPoint & p1)25 static void draw_extended_line(SkCanvas* canvas, const SkPaint paint,
26 const SkPoint& p0, const SkPoint& p1) {
27 SkVector v = p1 - p0;
28 v.setLength(v.length() + 3.f);
29 canvas->drawLine(p1 - v, p0 + v, paint);
30
31 // Draw normal vector too
32 SkPaint normalPaint = paint;
33 normalPaint.setPathEffect(nullptr);
34 normalPaint.setStrokeWidth(paint.getStrokeWidth() / 4.f);
35
36 SkVector n = {v.fY, -v.fX};
37 n.setLength(.25f);
38 SkPoint m = (p0 + p1) * 0.5f;
39 canvas->drawLine(m, m + n, normalPaint);
40 }
41
make_aa_line(const SkPoint & p0,const SkPoint & p1,bool aaOn,bool outset,SkPoint line[2])42 static void make_aa_line(const SkPoint& p0, const SkPoint& p1, bool aaOn,
43 bool outset, SkPoint line[2]) {
44 SkVector n = {0.f, 0.f};
45 if (aaOn) {
46 SkVector v = p1 - p0;
47 n = outset ? SkVector::Make(v.fY, -v.fX) : SkVector::Make(-v.fY, v.fX);
48 n.setLength(0.5f);
49 }
50
51 line[0] = p0 + n;
52 line[1] = p1 + n;
53 }
54
55 // To the line through l0-l1, not capped at the end points of the segment
signed_distance(const SkPoint & p,const SkPoint & l0,const SkPoint & l1)56 static SkScalar signed_distance(const SkPoint& p, const SkPoint& l0, const SkPoint& l1) {
57 SkVector v = l1 - l0;
58 v.normalize();
59 SkVector n = {v.fY, -v.fX};
60 SkScalar c = -n.dot(l0);
61 return n.dot(p) + c;
62 }
63
get_area_coverage(const bool edgeAA[4],const SkPoint corners[4],const SkPoint & point)64 static SkScalar get_area_coverage(const bool edgeAA[4], const SkPoint corners[4],
65 const SkPoint& point) {
66 SkPath shape;
67 shape.addPoly(corners, 4, true);
68 SkPath pixel;
69 pixel.addRect(SkRect::MakeXYWH(point.fX - 0.5f, point.fY - 0.5f, 1.f, 1.f));
70
71 SkPath intersection;
72 if (!Op(shape, pixel, kIntersect_SkPathOp, &intersection) || intersection.isEmpty()) {
73 return 0.f;
74 }
75
76 // Calculate area of the convex polygon
77 SkScalar area = 0.f;
78 for (int i = 0; i < intersection.countPoints(); ++i) {
79 SkPoint p0 = intersection.getPoint(i);
80 SkPoint p1 = intersection.getPoint((i + 1) % intersection.countPoints());
81 SkScalar det = p0.fX * p1.fY - p1.fX * p0.fY;
82 area += det;
83 }
84
85 // Scale by 1/2, then take abs value (this area formula is signed based on point winding, but
86 // since it's convex, just make it positive).
87 area = SkScalarAbs(0.5f * area);
88
89 // Now account for the edge AA. If the pixel center is outside of a non-AA edge, turn of its
90 // coverage. If the pixel only intersects non-AA edges, then set coverage to 1.
91 bool needsNonAA = false;
92 SkScalar edgeD[4];
93 for (int i = 0; i < 4; ++i) {
94 SkPoint e0 = corners[i];
95 SkPoint e1 = corners[(i + 1) % 4];
96 edgeD[i] = -signed_distance(point, e0, e1);
97 if (!edgeAA[i]) {
98 if (edgeD[i] < -1e-4f) {
99 return 0.f; // Outside of non-AA line
100 }
101 needsNonAA = true;
102 }
103 }
104 // Otherwise inside the shape, so check if any AA edge exerts influence over nonAA
105 if (needsNonAA) {
106 for (int i = 0; i < 4; i++) {
107 if (edgeAA[i] && edgeD[i] < 0.5f) {
108 needsNonAA = false;
109 break;
110 }
111 }
112 }
113 return needsNonAA ? 1.f : area;
114 }
115
116 // FIXME take into account max coverage properly,
get_edge_dist_coverage(const bool edgeAA[4],const SkPoint corners[4],const SkPoint outsetLines[8],const SkPoint insetLines[8],const SkPoint & point)117 static SkScalar get_edge_dist_coverage(const bool edgeAA[4], const SkPoint corners[4],
118 const SkPoint outsetLines[8], const SkPoint insetLines[8],
119 const SkPoint& point) {
120 bool flip = false;
121 // If the quad has been inverted, the original corners will not all be on the negative side of
122 // every outset line. When that happens, calculate coverage using the "inset" lines and flip
123 // the signed distance
124 for (int i = 0; i < 4; ++i) {
125 for (int j = 0; j < 4; ++j) {
126 SkScalar d = signed_distance(corners[i], outsetLines[j * 2], outsetLines[j * 2 + 1]);
127 if (d > 1e-4f) {
128 flip = true;
129 break;
130 }
131 }
132 if (flip) {
133 break;
134 }
135 }
136
137 const SkPoint* lines = flip ? insetLines : outsetLines;
138
139 SkScalar minCoverage = 1.f;
140 for (int i = 0; i < 4; ++i) {
141 // Multiply by negative 1 so that outside points have negative distances
142 SkScalar d = (flip ? 1 : -1) * signed_distance(point, lines[i * 2], lines[i * 2 + 1]);
143 if (!edgeAA[i] && d >= -1e-4f) {
144 d = 1.f;
145 }
146 if (d < minCoverage) {
147 minCoverage = d;
148 if (minCoverage < 0.f) {
149 break; // Outside the shape
150 }
151 }
152 }
153 return minCoverage < 0.f ? 0.f : minCoverage;
154 }
155
inside_triangle(const SkPoint & point,const SkPoint & t0,const SkPoint & t1,const SkPoint & t2,SkScalar bary[3])156 static bool inside_triangle(const SkPoint& point, const SkPoint& t0, const SkPoint& t1,
157 const SkPoint& t2, SkScalar bary[3]) {
158 // Check sign of t0 to (t1,t2). If it is positive, that means the normals point into the
159 // triangle otherwise the normals point outside the triangle so update edge distances as
160 // necessary
161 bool flip = signed_distance(t0, t1, t2) < 0.f;
162
163 SkScalar d0 = (flip ? -1 : 1) * signed_distance(point, t0, t1);
164 SkScalar d1 = (flip ? -1 : 1) * signed_distance(point, t1, t2);
165 SkScalar d2 = (flip ? -1 : 1) * signed_distance(point, t2, t0);
166 // Be a little forgiving
167 if (d0 < -1e-4f || d1 < -1e-4f || d2 < -1e-4f) {
168 return false;
169 }
170
171 // Inside, so calculate barycentric coords from the sideline distances
172 SkScalar d01 = (t0 - t1).length();
173 SkScalar d12 = (t1 - t2).length();
174 SkScalar d20 = (t2 - t0).length();
175
176 if (SkScalarNearlyZero(d12) || SkScalarNearlyZero(d20) || SkScalarNearlyZero(d01)) {
177 // Empty degenerate triangle
178 return false;
179 }
180
181 // Coordinates for a vertex use distances to the opposite edge
182 bary[0] = d1 * d12;
183 bary[1] = d2 * d20;
184 bary[2] = d0 * d01;
185 // And normalize
186 SkScalar sum = bary[0] + bary[1] + bary[2];
187 bary[0] /= sum;
188 bary[1] /= sum;
189 bary[2] /= sum;
190
191 return true;
192 }
193
get_framed_coverage(const SkPoint outer[4],const SkScalar outerCoverages[4],const SkPoint inner[4],const SkScalar innerCoverages[4],const SkRect & geomDomain,const SkPoint & point)194 static SkScalar get_framed_coverage(const SkPoint outer[4], const SkScalar outerCoverages[4],
195 const SkPoint inner[4], const SkScalar innerCoverages[4],
196 const SkRect& geomDomain, const SkPoint& point) {
197 // Triangles are ordered clock wise. Indices >= 4 refer to inner[i - 4]. Otherwise its outer[i].
198 static const int kFrameTris[] = {
199 0, 1, 4, 4, 1, 5,
200 1, 2, 5, 5, 2, 6,
201 2, 3, 6, 6, 3, 7,
202 3, 0, 7, 7, 0, 4,
203 4, 5, 7, 7, 5, 6
204 };
205 static const int kNumTris = 10;
206
207 SkScalar bary[3];
208 for (int i = 0; i < kNumTris; ++i) {
209 int i0 = kFrameTris[i * 3];
210 int i1 = kFrameTris[i * 3 + 1];
211 int i2 = kFrameTris[i * 3 + 2];
212
213 SkPoint t0 = i0 >= 4 ? inner[i0 - 4] : outer[i0];
214 SkPoint t1 = i1 >= 4 ? inner[i1 - 4] : outer[i1];
215 SkPoint t2 = i2 >= 4 ? inner[i2 - 4] : outer[i2];
216 if (inside_triangle(point, t0, t1, t2, bary)) {
217 // Calculate coverage by barycentric interpolation of coverages
218 SkScalar c0 = i0 >= 4 ? innerCoverages[i0 - 4] : outerCoverages[i0];
219 SkScalar c1 = i1 >= 4 ? innerCoverages[i1 - 4] : outerCoverages[i1];
220 SkScalar c2 = i2 >= 4 ? innerCoverages[i2 - 4] : outerCoverages[i2];
221
222 SkScalar coverage = bary[0] * c0 + bary[1] * c1 + bary[2] * c2;
223 if (coverage < 0.5f) {
224 // Check distances to domain
225 SkScalar l = SkTPin(point.fX - geomDomain.fLeft, 0.f, 1.f);
226 SkScalar t = SkTPin(point.fY - geomDomain.fTop, 0.f, 1.f);
227 SkScalar r = SkTPin(geomDomain.fRight - point.fX, 0.f, 1.f);
228 SkScalar b = SkTPin(geomDomain.fBottom - point.fY, 0.f, 1.f);
229 coverage = std::min(coverage, l * t * r * b);
230 }
231 return coverage;
232 }
233 }
234 // Not inside any triangle
235 return 0.f;
236 }
237
238 static constexpr SkScalar kViewScale = 100.f;
239 static constexpr SkScalar kViewOffset = 200.f;
240
241 class DegenerateQuadSample : public Sample {
242 public:
DegenerateQuadSample(const SkRect & rect)243 DegenerateQuadSample(const SkRect& rect)
244 : fOuterRect(rect)
245 , fCoverageMode(CoverageMode::kArea) {
246 fOuterRect.toQuad(fCorners);
247 for (int i = 0; i < 4; ++i) {
248 fEdgeAA[i] = true;
249 }
250 }
251
onDrawContent(SkCanvas * canvas)252 void onDrawContent(SkCanvas* canvas) override {
253 static const SkScalar kDotParams[2] = {1.f / kViewScale, 12.f / kViewScale};
254 sk_sp<SkPathEffect> dots = SkDashPathEffect::Make(kDotParams, 2, 0.f);
255 static const SkScalar kDashParams[2] = {8.f / kViewScale, 12.f / kViewScale};
256 sk_sp<SkPathEffect> dashes = SkDashPathEffect::Make(kDashParams, 2, 0.f);
257
258 SkPaint circlePaint;
259 circlePaint.setAntiAlias(true);
260
261 SkPaint linePaint;
262 linePaint.setAntiAlias(true);
263 linePaint.setStyle(SkPaint::kStroke_Style);
264 linePaint.setStrokeWidth(4.f / kViewScale);
265 linePaint.setStrokeJoin(SkPaint::kRound_Join);
266 linePaint.setStrokeCap(SkPaint::kRound_Cap);
267
268 canvas->translate(kViewOffset, kViewOffset);
269 canvas->scale(kViewScale, kViewScale);
270
271 // Draw the outer rectangle as a dotted line
272 linePaint.setPathEffect(dots);
273 canvas->drawRect(fOuterRect, linePaint);
274
275 bool valid = this->isValid();
276
277 if (valid) {
278 SkPoint outsets[8];
279 SkPoint insets[8];
280 // Calculate inset and outset lines for edge-distance visualization
281 for (int i = 0; i < 4; ++i) {
282 make_aa_line(fCorners[i], fCorners[(i + 1) % 4], fEdgeAA[i], true, outsets + i * 2);
283 make_aa_line(fCorners[i], fCorners[(i + 1) % 4], fEdgeAA[i], false, insets + i * 2);
284 }
285
286 // Calculate inner and outer meshes for GPU visualization
287 SkPoint gpuOutset[4];
288 SkScalar gpuOutsetCoverage[4];
289 SkPoint gpuInset[4];
290 SkScalar gpuInsetCoverage[4];
291 SkRect gpuDomain;
292 this->getTessellatedPoints(gpuInset, gpuInsetCoverage, gpuOutset, gpuOutsetCoverage,
293 &gpuDomain);
294
295 // Visualize the coverage values across the clamping rectangle, but test pixels outside
296 // of the "outer" rect since some quad edges can be outset extra far.
297 SkPaint pixelPaint;
298 pixelPaint.setAntiAlias(true);
299 SkRect covRect = fOuterRect.makeOutset(2.f, 2.f);
300 for (SkScalar py = covRect.fTop; py < covRect.fBottom; py += 1.f) {
301 for (SkScalar px = covRect.fLeft; px < covRect.fRight; px += 1.f) {
302 // px and py are the top-left corner of the current pixel, so get center's
303 // coordinate
304 SkPoint pixelCenter = {px + 0.5f, py + 0.5f};
305 SkScalar coverage;
306 if (fCoverageMode == CoverageMode::kArea) {
307 coverage = get_area_coverage(fEdgeAA, fCorners, pixelCenter);
308 } else if (fCoverageMode == CoverageMode::kEdgeDistance) {
309 coverage = get_edge_dist_coverage(fEdgeAA, fCorners, outsets, insets,
310 pixelCenter);
311 } else {
312 SkASSERT(fCoverageMode == CoverageMode::kGPUMesh);
313 coverage = get_framed_coverage(gpuOutset, gpuOutsetCoverage,
314 gpuInset, gpuInsetCoverage, gpuDomain,
315 pixelCenter);
316 }
317
318 SkRect pixelRect = SkRect::MakeXYWH(px, py, 1.f, 1.f);
319 pixelRect.inset(0.1f, 0.1f);
320
321 SkScalar a = 1.f - 0.5f * coverage;
322 pixelPaint.setColor4f({a, a, a, 1.f}, nullptr);
323 canvas->drawRect(pixelRect, pixelPaint);
324
325 pixelPaint.setColor(coverage > 0.f ? SK_ColorGREEN : SK_ColorRED);
326 pixelRect.inset(0.38f, 0.38f);
327 canvas->drawRect(pixelRect, pixelPaint);
328 }
329 }
330
331 linePaint.setPathEffect(dashes);
332 // Draw the inset/outset "infinite" lines
333 if (fCoverageMode == CoverageMode::kEdgeDistance) {
334 for (int i = 0; i < 4; ++i) {
335 if (fEdgeAA[i]) {
336 linePaint.setColor(SK_ColorBLUE);
337 draw_extended_line(canvas, linePaint, outsets[i * 2], outsets[i * 2 + 1]);
338 linePaint.setColor(SK_ColorGREEN);
339 draw_extended_line(canvas, linePaint, insets[i * 2], insets[i * 2 + 1]);
340 } else {
341 // Both outset and inset are the same line, so only draw one in cyan
342 linePaint.setColor(SK_ColorCYAN);
343 draw_extended_line(canvas, linePaint, outsets[i * 2], outsets[i * 2 + 1]);
344 }
345 }
346 }
347
348 linePaint.setPathEffect(nullptr);
349 // What is tessellated using GrQuadPerEdgeAA
350 if (fCoverageMode == CoverageMode::kGPUMesh) {
351 SkPath outsetPath;
352 outsetPath.addPoly(gpuOutset, 4, true);
353 linePaint.setColor(SK_ColorBLUE);
354 canvas->drawPath(outsetPath, linePaint);
355
356 SkPath insetPath;
357 insetPath.addPoly(gpuInset, 4, true);
358 linePaint.setColor(SK_ColorGREEN);
359 canvas->drawPath(insetPath, linePaint);
360
361 SkPaint domainPaint = linePaint;
362 domainPaint.setStrokeWidth(2.f / kViewScale);
363 domainPaint.setPathEffect(dashes);
364 domainPaint.setColor(SK_ColorMAGENTA);
365 canvas->drawRect(gpuDomain, domainPaint);
366 }
367
368 // Draw the edges of the true quad as a solid line
369 SkPath path;
370 path.addPoly(fCorners, 4, true);
371 linePaint.setColor(SK_ColorBLACK);
372 canvas->drawPath(path, linePaint);
373 } else {
374 // Draw the edges of the true quad as a solid *red* line
375 SkPath path;
376 path.addPoly(fCorners, 4, true);
377 linePaint.setColor(SK_ColorRED);
378 linePaint.setPathEffect(nullptr);
379 canvas->drawPath(path, linePaint);
380 }
381
382 // Draw the four clickable corners as circles
383 circlePaint.setColor(valid ? SK_ColorBLACK : SK_ColorRED);
384 for (int i = 0; i < 4; ++i) {
385 canvas->drawCircle(fCorners[i], 5.f / kViewScale, circlePaint);
386 }
387 }
388
389 Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) override;
390 bool onClick(Sample::Click*) override;
391 bool onChar(SkUnichar) override;
name()392 SkString name() override { return SkString("DegenerateQuad"); }
393
394 private:
395 class Click;
396
397 enum class CoverageMode {
398 kArea, kEdgeDistance, kGPUMesh
399 };
400
401 const SkRect fOuterRect;
402 SkPoint fCorners[4]; // TL, TR, BR, BL
403 bool fEdgeAA[4]; // T, R, B, L
404 CoverageMode fCoverageMode;
405
isValid() const406 bool isValid() const {
407 SkPath path;
408 path.addPoly(fCorners, 4, true);
409 return path.isConvex();
410 }
411
getTessellatedPoints(SkPoint inset[4],SkScalar insetCoverage[4],SkPoint outset[4],SkScalar outsetCoverage[4],SkRect * domain) const412 void getTessellatedPoints(SkPoint inset[4], SkScalar insetCoverage[4], SkPoint outset[4],
413 SkScalar outsetCoverage[4], SkRect* domain) const {
414 // Fixed vertex spec for extracting the picture frame geometry
415 static const VertexSpec kSpec =
416 {GrQuad::Type::kGeneral, ColorType::kNone,
417 GrQuad::Type::kAxisAligned, false, Subset::kNo,
418 GrAAType::kCoverage, false, IndexBufferOption::kPictureFramed};
419 static const GrQuad kIgnored(SkRect::MakeEmpty());
420
421 GrQuadAAFlags flags = GrQuadAAFlags::kNone;
422 flags |= fEdgeAA[0] ? GrQuadAAFlags::kTop : GrQuadAAFlags::kNone;
423 flags |= fEdgeAA[1] ? GrQuadAAFlags::kRight : GrQuadAAFlags::kNone;
424 flags |= fEdgeAA[2] ? GrQuadAAFlags::kBottom : GrQuadAAFlags::kNone;
425 flags |= fEdgeAA[3] ? GrQuadAAFlags::kLeft : GrQuadAAFlags::kNone;
426
427 GrQuad quad = GrQuad::MakeFromSkQuad(fCorners, SkMatrix::I());
428
429 float vertices[56]; // 2 quads, with x, y, coverage, and geometry domain (7 floats x 8 vert)
430 skgpu::v1::QuadPerEdgeAA::Tessellator tessellator(kSpec, (char*) vertices);
431 tessellator.append(&quad, nullptr, {1.f, 1.f, 1.f, 1.f},
432 SkRect::MakeEmpty(), flags);
433
434 // The first quad in vertices is the inset, then the outset, but they
435 // are ordered TL, BL, TR, BR so un-interleave coverage and re-arrange
436 inset[0] = {vertices[0], vertices[1]}; // TL
437 insetCoverage[0] = vertices[2];
438 inset[3] = {vertices[7], vertices[8]}; // BL
439 insetCoverage[3] = vertices[9];
440 inset[1] = {vertices[14], vertices[15]}; // TR
441 insetCoverage[1] = vertices[16];
442 inset[2] = {vertices[21], vertices[22]}; // BR
443 insetCoverage[2] = vertices[23];
444
445 outset[0] = {vertices[28], vertices[29]}; // TL
446 outsetCoverage[0] = vertices[30];
447 outset[3] = {vertices[35], vertices[36]}; // BL
448 outsetCoverage[3] = vertices[37];
449 outset[1] = {vertices[42], vertices[43]}; // TR
450 outsetCoverage[1] = vertices[44];
451 outset[2] = {vertices[49], vertices[50]}; // BR
452 outsetCoverage[2] = vertices[51];
453
454 *domain = {vertices[52], vertices[53], vertices[54], vertices[55]};
455 }
456
457 using INHERITED = Sample;
458 };
459
460 class DegenerateQuadSample::Click : public Sample::Click {
461 public:
Click(const SkRect & clamp,int index)462 Click(const SkRect& clamp, int index)
463 : fOuterRect(clamp)
464 , fIndex(index) {}
465
doClick(SkPoint points[4])466 void doClick(SkPoint points[4]) {
467 if (fIndex >= 0) {
468 this->drag(&points[fIndex]);
469 } else {
470 for (int i = 0; i < 4; ++i) {
471 this->drag(&points[i]);
472 }
473 }
474 }
475
476 private:
477 SkRect fOuterRect;
478 int fIndex;
479
drag(SkPoint * point)480 void drag(SkPoint* point) {
481 SkPoint delta = fCurr - fPrev;
482 *point += SkPoint::Make(delta.x() / kViewScale, delta.y() / kViewScale);
483 point->fX = std::min(fOuterRect.fRight, std::max(point->fX, fOuterRect.fLeft));
484 point->fY = std::min(fOuterRect.fBottom, std::max(point->fY, fOuterRect.fTop));
485 }
486 };
487
onFindClickHandler(SkScalar x,SkScalar y,skui::ModifierKey)488 Sample::Click* DegenerateQuadSample::onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) {
489 SkPoint inCTM = SkPoint::Make((x - kViewOffset) / kViewScale, (y - kViewOffset) / kViewScale);
490 for (int i = 0; i < 4; ++i) {
491 if ((fCorners[i] - inCTM).length() < 10.f / kViewScale) {
492 return new Click(fOuterRect, i);
493 }
494 }
495 return new Click(fOuterRect, -1);
496 }
497
onClick(Sample::Click * click)498 bool DegenerateQuadSample::onClick(Sample::Click* click) {
499 Click* myClick = (Click*) click;
500 myClick->doClick(fCorners);
501 return true;
502 }
503
onChar(SkUnichar code)504 bool DegenerateQuadSample::onChar(SkUnichar code) {
505 switch(code) {
506 case '1':
507 fEdgeAA[0] = !fEdgeAA[0];
508 return true;
509 case '2':
510 fEdgeAA[1] = !fEdgeAA[1];
511 return true;
512 case '3':
513 fEdgeAA[2] = !fEdgeAA[2];
514 return true;
515 case '4':
516 fEdgeAA[3] = !fEdgeAA[3];
517 return true;
518 case 'q':
519 fCoverageMode = CoverageMode::kArea;
520 return true;
521 case 'w':
522 fCoverageMode = CoverageMode::kEdgeDistance;
523 return true;
524 case 'e':
525 fCoverageMode = CoverageMode::kGPUMesh;
526 return true;
527 }
528 return false;
529 }
530
531 DEF_SAMPLE(return new DegenerateQuadSample(SkRect::MakeWH(4.f, 4.f));)
532