• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "include/core/SkCanvas.h"
11 #include "include/core/SkColorFilter.h"
12 #include "include/core/SkFont.h"
13 #include "include/core/SkImage.h"
14 #include "include/core/SkPath.h"
15 #include "include/core/SkSurface.h"
16 
17 namespace skiagm {
18 
19 class ShapeRenderer : public SkRefCntBase {
20 public:
21     inline static constexpr SkScalar kTileWidth = 20.f;
22     inline static constexpr SkScalar kTileHeight = 20.f;
23 
24     // Draw the shape, limited to kTileWidth x kTileHeight. It must apply the local subpixel (tx,
25     // ty) translation and rotation by angle. Prior to these transform adjustments, the SkCanvas
26     // will only have pixel aligned translations (these are separated to make super-sampling
27     // renderers easier).
28     virtual void draw(SkCanvas* canvas, SkPaint* paint,
29                       SkScalar tx, SkScalar ty, SkScalar angle) = 0;
30 
31     virtual SkString name() = 0;
32 
33     virtual sk_sp<ShapeRenderer> toHairline() = 0;
34 
applyLocalTransform(SkCanvas * canvas,SkScalar tx,SkScalar ty,SkScalar angle)35     void applyLocalTransform(SkCanvas* canvas, SkScalar tx, SkScalar ty, SkScalar angle) {
36         canvas->translate(tx, ty);
37         canvas->rotate(angle, kTileWidth / 2.f, kTileHeight / 2.f);
38     }
39 };
40 
41 class RectRenderer : public ShapeRenderer {
42 public:
Make()43     static sk_sp<ShapeRenderer> Make() {
44         return sk_sp<ShapeRenderer>(new RectRenderer());
45     }
46 
name()47     SkString name() override { return SkString("rect"); }
48 
toHairline()49     sk_sp<ShapeRenderer> toHairline() override {
50         // Not really available but can't return nullptr
51         return Make();
52     }
53 
draw(SkCanvas * canvas,SkPaint * paint,SkScalar tx,SkScalar ty,SkScalar angle)54     void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
55         SkScalar width = paint->getStrokeWidth();
56         paint->setStyle(SkPaint::kFill_Style);
57 
58         this->applyLocalTransform(canvas, tx, ty, angle);
59         canvas->drawRect(SkRect::MakeLTRB(kTileWidth / 2.f - width / 2.f, 2.f,
60                                           kTileWidth / 2.f + width / 2.f, kTileHeight - 2.f),
61                          *paint);
62     }
63 
64 private:
RectRenderer()65     RectRenderer() {}
66 
67     using INHERITED = ShapeRenderer;
68 };
69 
70 class PathRenderer : public ShapeRenderer {
71 public:
MakeLine(bool hairline=false)72     static sk_sp<ShapeRenderer> MakeLine(bool hairline = false) {
73         return MakeCurve(0.f, hairline);
74     }
75 
MakeLines(SkScalar depth,bool hairline=false)76     static sk_sp<ShapeRenderer> MakeLines(SkScalar depth, bool hairline = false) {
77         return MakeCurve(-depth, hairline);
78     }
79 
MakeCurve(SkScalar depth,bool hairline=false)80     static sk_sp<ShapeRenderer> MakeCurve(SkScalar depth, bool hairline = false) {
81         return sk_sp<ShapeRenderer>(new PathRenderer(depth, hairline));
82     }
83 
name()84     SkString name() override {
85         SkString name;
86         if (fHairline) {
87             name.append("hairline");
88             if (fDepth > 0.f) {
89                 name.appendf("-curve-%.2f", fDepth);
90             }
91         } else if (fDepth > 0.f) {
92             name.appendf("curve-%.2f", fDepth);
93         } else if (fDepth < 0.f) {
94             name.appendf("line-%.2f", -fDepth);
95         } else {
96             name.append("line");
97         }
98 
99         return name;
100     }
101 
toHairline()102     sk_sp<ShapeRenderer> toHairline() override {
103         return sk_sp<ShapeRenderer>(new PathRenderer(fDepth, true));
104     }
105 
draw(SkCanvas * canvas,SkPaint * paint,SkScalar tx,SkScalar ty,SkScalar angle)106     void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
107         SkPath path;
108         path.moveTo(kTileWidth / 2.f, 2.f);
109 
110         if (fDepth > 0.f) {
111             path.quadTo(kTileWidth / 2.f + fDepth, kTileHeight / 2.f,
112                         kTileWidth / 2.f, kTileHeight - 2.f);
113         } else {
114             if (fDepth < 0.f) {
115                 path.lineTo(kTileWidth / 2.f + fDepth, kTileHeight / 2.f);
116             }
117             path.lineTo(kTileWidth / 2.f, kTileHeight - 2.f);
118         }
119 
120         if (fHairline) {
121             // Fake thinner hairlines by making it transparent, conflating coverage and alpha
122             SkColor4f color = paint->getColor4f();
123             SkScalar width = paint->getStrokeWidth();
124             if (width > 1.f) {
125                 // Can't emulate width larger than a pixel
126                 return;
127             }
128             paint->setColor4f({color.fR, color.fG, color.fB, width}, nullptr);
129             paint->setStrokeWidth(0.f);
130         }
131 
132         // Adding round caps forces Ganesh to use the path renderer for lines instead of converting
133         // them to rectangles (which are already explicitly tested). However, when not curved, the
134         // GrStyledShape will still find a way to turn it into a rrect draw so it doesn't hit the
135         // path renderer in that condition.
136         paint->setStrokeCap(SkPaint::kRound_Cap);
137         paint->setStrokeJoin(SkPaint::kMiter_Join);
138         paint->setStyle(SkPaint::kStroke_Style);
139 
140         this->applyLocalTransform(canvas, tx, ty, angle);
141         canvas->drawPath(path, *paint);
142     }
143 
144 private:
145     SkScalar fDepth; // 0.f to make a line, otherwise outset of curve from end points
146     bool fHairline;
147 
PathRenderer(SkScalar depth,bool hairline)148     PathRenderer(SkScalar depth, bool hairline)
149             : fDepth(depth)
150             , fHairline(hairline) {}
151 
152     using INHERITED = ShapeRenderer;
153 };
154 
155 class OffscreenShapeRenderer : public ShapeRenderer {
156 public:
157     ~OffscreenShapeRenderer() override = default;
158 
Make(sk_sp<ShapeRenderer> renderer,int supersample,bool forceRaster=false)159     static sk_sp<OffscreenShapeRenderer> Make(sk_sp<ShapeRenderer> renderer, int supersample,
160                                               bool forceRaster = false) {
161         SkASSERT(supersample > 0);
162         return sk_sp<OffscreenShapeRenderer>(new OffscreenShapeRenderer(std::move(renderer),
163                                                                         supersample, forceRaster));
164     }
165 
name()166     SkString name() override {
167         SkString name = fRenderer->name();
168         if (fSupersampleFactor != 1) {
169             name.prependf("%dx-", fSupersampleFactor * fSupersampleFactor);
170         }
171         return name;
172     }
173 
toHairline()174     sk_sp<ShapeRenderer> toHairline() override {
175         return Make(fRenderer->toHairline(), fSupersampleFactor, fForceRasterBackend);
176     }
177 
draw(SkCanvas * canvas,SkPaint * paint,SkScalar tx,SkScalar ty,SkScalar angle)178     void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
179         // Subpixel translation+angle are applied in the offscreen buffer
180         this->prepareBuffer(canvas, paint, tx, ty, angle);
181         this->redraw(canvas);
182     }
183 
184     // Exposed so that it's easy to fill the offscreen buffer, then draw zooms/filters of it before
185     // drawing the original scale back into the canvas.
prepareBuffer(SkCanvas * canvas,SkPaint * paint,SkScalar tx,SkScalar ty,SkScalar angle)186     void prepareBuffer(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) {
187         auto info = SkImageInfo::Make(fSupersampleFactor * kTileWidth,
188                                       fSupersampleFactor * kTileHeight,
189                                       kRGBA_8888_SkColorType, kPremul_SkAlphaType);
190         auto surface = fForceRasterBackend ? SkSurface::MakeRaster(info)
191                                            : canvas->makeSurface(info);
192 
193         surface->getCanvas()->save();
194         // Make fully transparent so it is easy to determine pixels that are touched by partial cov.
195         surface->getCanvas()->clear(SK_ColorTRANSPARENT);
196         // Set up scaling to fit supersampling amount
197         surface->getCanvas()->scale(fSupersampleFactor, fSupersampleFactor);
198         fRenderer->draw(surface->getCanvas(), paint, tx, ty, angle);
199         surface->getCanvas()->restore();
200 
201         // Save image so it can be drawn zoomed in or to visualize touched pixels; only valid until
202         // the next call to draw()
203         fLastRendered = surface->makeImageSnapshot();
204     }
205 
redraw(SkCanvas * canvas,SkScalar scale=1.f,bool debugMode=false)206     void redraw(SkCanvas* canvas, SkScalar scale = 1.f, bool debugMode = false) {
207         SkASSERT(fLastRendered);
208         // Use medium quality filter to get mipmaps when drawing smaller, or use nearest filtering
209         // when upscaling
210         SkPaint blit;
211         if (debugMode) {
212             // Makes anything that's > 1/255 alpha fully opaque and sets color to medium green.
213             static constexpr float kFilter[] = {
214                 0.f, 0.f, 0.f, 0.f, 16.f/255,
215                 0.f, 0.f, 0.f, 0.f, 200.f/255,
216                 0.f, 0.f, 0.f, 0.f, 16.f/255,
217                 0.f, 0.f, 0.f, 255.f, 0.f
218             };
219 
220             blit.setColorFilter(SkColorFilters::Matrix(kFilter));
221         }
222 
223         auto sampling = scale > 1 ? SkSamplingOptions(SkFilterMode::kNearest)
224                                   : SkSamplingOptions(SkFilterMode::kLinear,
225                                                       SkMipmapMode::kLinear);
226 
227         canvas->scale(scale, scale);
228         canvas->drawImageRect(fLastRendered.get(),
229                               SkRect::MakeWH(kTileWidth, kTileHeight),
230                               SkRect::MakeWH(kTileWidth, kTileHeight),
231                               sampling, &blit, SkCanvas::kFast_SrcRectConstraint);
232     }
233 
234 private:
235     bool                 fForceRasterBackend;
236     sk_sp<SkImage>       fLastRendered;
237     sk_sp<ShapeRenderer> fRenderer;
238     int                  fSupersampleFactor;
239 
OffscreenShapeRenderer(sk_sp<ShapeRenderer> renderer,int supersample,bool forceRaster)240     OffscreenShapeRenderer(sk_sp<ShapeRenderer> renderer, int supersample, bool forceRaster)
241             : fForceRasterBackend(forceRaster)
242             , fLastRendered(nullptr)
243             , fRenderer(std::move(renderer))
244             , fSupersampleFactor(supersample) { }
245 
246     using INHERITED = ShapeRenderer;
247 };
248 
249 class ThinAASample : public Sample {
250 public:
ThinAASample()251     ThinAASample() {
252         this->setBGColor(0xFFFFFFFF);
253     }
254 
255 protected:
onOnceBeforeDraw()256     void onOnceBeforeDraw() override {
257         // Setup all base renderers
258         fShapes.push_back(RectRenderer::Make());
259         fShapes.push_back(PathRenderer::MakeLine());
260         fShapes.push_back(PathRenderer::MakeLines(4.f)); // 2 segments
261         fShapes.push_back(PathRenderer::MakeCurve(2.f)); // Shallow curve
262         fShapes.push_back(PathRenderer::MakeCurve(8.f)); // Deep curve
263 
264         for (int i = 0; i < fShapes.count(); ++i) {
265             fNative.push_back(OffscreenShapeRenderer::Make(fShapes[i], 1));
266             fRaster.push_back(OffscreenShapeRenderer::Make(fShapes[i], 1, /* raster */ true));
267             fSS4.push_back(OffscreenShapeRenderer::Make(fShapes[i], 4)); // 4x4 -> 16 samples
268             fSS16.push_back(OffscreenShapeRenderer::Make(fShapes[i], 8)); // 8x8 -> 64 samples
269 
270             fHairline.push_back(OffscreenShapeRenderer::Make(fRaster[i]->toHairline(), 1));
271         }
272 
273         // Start it at something subpixel
274         fStrokeWidth = 0.5f;
275 
276         fSubpixelX = 0.f;
277         fSubpixelY = 0.f;
278         fAngle = 0.f;
279 
280         fCurrentStage = AnimStage::kMoveLeft;
281         fLastFrameTime = -1.f;
282 
283         // Don't animate in the beginning
284         fAnimTranslate = false;
285         fAnimRotate = false;
286     }
287 
onDrawContent(SkCanvas * canvas)288     void onDrawContent(SkCanvas* canvas) override {
289         // Move away from screen edge and add instructions
290         SkPaint text;
291         SkFont font(nullptr, 12);
292         canvas->translate(60.f, 20.f);
293         canvas->drawString("Each row features a rendering command under different AA strategies. "
294                            "Native refers to the current backend of the viewer, e.g. OpenGL.",
295                            0, 0, font, text);
296 
297         canvas->drawString(SkStringPrintf("Stroke width: %.2f ('-' to decrease, '=' to increase)",
298                 fStrokeWidth), 0, 24, font, text);
299         canvas->drawString(SkStringPrintf("Rotation: %.3f ('r' to animate, 'y' sets to 90, 'u' sets"
300                 " to 0, 'space' adds 15)", fAngle), 0, 36, font, text);
301         canvas->drawString(SkStringPrintf("Translation: %.3f, %.3f ('t' to animate)",
302                 fSubpixelX, fSubpixelY), 0, 48, font, text);
303 
304         canvas->translate(0.f, 100.f);
305 
306         // Draw with surface matching current viewer surface type
307         this->drawShapes(canvas, "Native", 0, fNative);
308 
309         // Draw with forced raster backend so it's easy to compare side-by-side
310         this->drawShapes(canvas, "Raster", 1, fRaster);
311 
312         // Draw paths as hairlines + alpha hack
313         this->drawShapes(canvas, "Hairline", 2, fHairline);
314 
315         // Draw at 4x supersampling in bottom left
316         this->drawShapes(canvas, "SSx16", 3, fSS4);
317 
318         // And lastly 16x supersampling in bottom right
319         this->drawShapes(canvas, "SSx64", 4, fSS16);
320     }
321 
onAnimate(double nanos)322     bool onAnimate(double nanos) override {
323         SkScalar t = 1e-9 * nanos;
324         SkScalar dt = fLastFrameTime < 0.f ? 0.f : t - fLastFrameTime;
325         fLastFrameTime = t;
326 
327         if (!fAnimRotate && !fAnimTranslate) {
328             // Keep returning true so that the last frame time is tracked
329             fLastFrameTime = -1.f;
330             return false;
331         }
332 
333         switch(fCurrentStage) {
334             case AnimStage::kMoveLeft:
335                 fSubpixelX += 2.f * dt;
336                 if (fSubpixelX >= 1.f) {
337                     fSubpixelX = 1.f;
338                     fCurrentStage = AnimStage::kMoveDown;
339                 }
340                 break;
341             case AnimStage::kMoveDown:
342                 fSubpixelY += 2.f * dt;
343                 if (fSubpixelY >= 1.f) {
344                     fSubpixelY = 1.f;
345                     fCurrentStage = AnimStage::kMoveRight;
346                 }
347                 break;
348             case AnimStage::kMoveRight:
349                 fSubpixelX -= 2.f * dt;
350                 if (fSubpixelX <= -1.f) {
351                     fSubpixelX = -1.f;
352                     fCurrentStage = AnimStage::kMoveUp;
353                 }
354                 break;
355             case AnimStage::kMoveUp:
356                 fSubpixelY -= 2.f * dt;
357                 if (fSubpixelY <= -1.f) {
358                     fSubpixelY = -1.f;
359                     fCurrentStage = fAnimRotate ? AnimStage::kRotate : AnimStage::kMoveLeft;
360                 }
361                 break;
362             case AnimStage::kRotate: {
363                 SkScalar newAngle = fAngle + dt * 15.f;
364                 bool completed = SkScalarMod(newAngle, 15.f) < SkScalarMod(fAngle, 15.f);
365                 fAngle = SkScalarMod(newAngle, 360.f);
366                 if (completed) {
367                     // Make sure we're on a 15 degree boundary
368                     fAngle = 15.f * SkScalarRoundToScalar(fAngle / 15.f);
369                     if (fAnimTranslate) {
370                         fCurrentStage = this->getTranslationStage();
371                     }
372                 }
373             } break;
374         }
375 
376         return true;
377     }
378 
name()379     SkString name() override { return SkString("Thin-AA"); }
380 
onChar(SkUnichar key)381     bool onChar(SkUnichar key) override {
382             switch(key) {
383                 case 't':
384                     // Toggle translation animation.
385                     fAnimTranslate = !fAnimTranslate;
386                     if (!fAnimTranslate && fAnimRotate && fCurrentStage != AnimStage::kRotate) {
387                         // Turned off an active translation so go to rotating
388                         fCurrentStage = AnimStage::kRotate;
389                     } else if (fAnimTranslate && !fAnimRotate &&
390                                fCurrentStage == AnimStage::kRotate) {
391                         // Turned on translation, rotation had been paused too, so reset the stage
392                         fCurrentStage = this->getTranslationStage();
393                     }
394                     return true;
395                 case 'r':
396                     // Toggle rotation animation.
397                     fAnimRotate = !fAnimRotate;
398                     if (!fAnimRotate && fAnimTranslate && fCurrentStage == AnimStage::kRotate) {
399                         // Turned off an active rotation so go back to translation
400                         fCurrentStage = this->getTranslationStage();
401                     } else if (fAnimRotate && !fAnimTranslate &&
402                                fCurrentStage != AnimStage::kRotate) {
403                         // Turned on rotation, translation had been paused too, so reset to rotate
404                         fCurrentStage = AnimStage::kRotate;
405                     }
406                     return true;
407                 case 'u': fAngle = 0.f; return true;
408                 case 'y': fAngle = 90.f; return true;
409                 case ' ': fAngle = SkScalarMod(fAngle + 15.f, 360.f); return true;
410                 case '-': fStrokeWidth = std::max(0.1f, fStrokeWidth - 0.05f); return true;
411                 case '=': fStrokeWidth = std::min(1.f, fStrokeWidth + 0.05f); return true;
412             }
413             return false;
414     }
415 
416 private:
417     // Base renderers that get wrapped on the offscreen renderers so that they can be transformed
418     // for visualization, or supersampled.
419     SkTArray<sk_sp<ShapeRenderer>> fShapes;
420 
421     SkTArray<sk_sp<OffscreenShapeRenderer>> fNative;
422     SkTArray<sk_sp<OffscreenShapeRenderer>> fRaster;
423     SkTArray<sk_sp<OffscreenShapeRenderer>> fHairline;
424     SkTArray<sk_sp<OffscreenShapeRenderer>> fSS4;
425     SkTArray<sk_sp<OffscreenShapeRenderer>> fSS16;
426 
427     SkScalar fStrokeWidth;
428 
429     // Animated properties to stress the AA algorithms
430     enum class AnimStage {
431         kMoveRight, kMoveDown, kMoveLeft, kMoveUp, kRotate
432     } fCurrentStage;
433     SkScalar fLastFrameTime;
434     bool     fAnimRotate;
435     bool     fAnimTranslate;
436 
437     // Current frame's animation state
438     SkScalar fSubpixelX;
439     SkScalar fSubpixelY;
440     SkScalar fAngle;
441 
getTranslationStage()442     AnimStage getTranslationStage() {
443         // For paused translations (i.e. fAnimTranslate toggled while translating), the current
444         // stage moves to kRotate, but when restarting the translation animation, we want to
445         // go back to where we were without losing any progress.
446         if (fSubpixelX > -1.f) {
447             if (fSubpixelX >= 1.f) {
448                 // Can only be moving down on right edge, given our transition states
449                 return AnimStage::kMoveDown;
450             } else if (fSubpixelY > 0.f) {
451                 // Can only be moving right along top edge
452                 return AnimStage::kMoveRight;
453             } else {
454                 // Must be moving left along bottom edge
455                 return AnimStage::kMoveLeft;
456             }
457         } else {
458             // Moving up along the left edge, or is at the very top so start moving left
459             return fSubpixelY > -1.f ? AnimStage::kMoveUp : AnimStage::kMoveLeft;
460         }
461     }
462 
drawShapes(SkCanvas * canvas,const char * name,int gridX,SkTArray<sk_sp<OffscreenShapeRenderer>> shapes)463     void drawShapes(SkCanvas* canvas, const char* name, int gridX,
464                     SkTArray<sk_sp<OffscreenShapeRenderer>> shapes) {
465         SkAutoCanvasRestore autoRestore(canvas, /* save */ true);
466 
467         for (int i = 0; i < shapes.count(); ++i) {
468             this->drawShape(canvas, name, gridX, shapes[i].get(), i == 0);
469             // drawShape positions the canvas properly for the next iteration
470         }
471     }
472 
drawShape(SkCanvas * canvas,const char * name,int gridX,OffscreenShapeRenderer * shape,bool drawNameLabels)473     void drawShape(SkCanvas* canvas, const char* name, int gridX,
474                    OffscreenShapeRenderer* shape, bool drawNameLabels) {
475         static constexpr SkScalar kZoomGridWidth = 8 * ShapeRenderer::kTileWidth + 8.f;
476         static constexpr SkRect kTile = SkRect::MakeWH(ShapeRenderer::kTileWidth,
477                                                        ShapeRenderer::kTileHeight);
478         static constexpr SkRect kZoomTile = SkRect::MakeWH(8 * ShapeRenderer::kTileWidth,
479                                                            8 * ShapeRenderer::kTileHeight);
480 
481         // Labeling per shape and detailed labeling that isn't per-stroke
482         canvas->save();
483         SkPaint text;
484         SkFont font(nullptr, 12);
485 
486         if (gridX == 0) {
487             SkScalar centering = shape->name().size() * 4.f; // ad-hoc
488 
489             canvas->save();
490             canvas->translate(-10.f, 4 * ShapeRenderer::kTileHeight + centering);
491             canvas->rotate(-90.f);
492             canvas->drawString(shape->name(), 0.f, 0.f, font, text);
493             canvas->restore();
494         }
495         if (drawNameLabels) {
496             canvas->drawString(name, gridX * kZoomGridWidth, -10.f, font, text);
497         }
498         canvas->restore();
499 
500         // Paints for outlines and actual shapes
501         SkPaint outline;
502         outline.setStyle(SkPaint::kStroke_Style);
503         SkPaint clear;
504         clear.setColor(SK_ColorWHITE);
505 
506         SkPaint paint;
507         paint.setAntiAlias(true);
508         paint.setStrokeWidth(fStrokeWidth);
509 
510         // Generate a saved image of the correct stroke width, but don't put it into the canvas
511         // yet since we want to draw the "original" size on top of the zoomed in version
512         shape->prepareBuffer(canvas, &paint, fSubpixelX, fSubpixelY, fAngle);
513 
514         // Draw it at 8X zoom
515         SkScalar x = gridX * kZoomGridWidth;
516 
517         canvas->save();
518         canvas->translate(x, 0.f);
519         canvas->drawRect(kZoomTile, outline);
520         shape->redraw(canvas, 8.0f);
521         canvas->restore();
522 
523         // Draw the original
524         canvas->save();
525         canvas->translate(x + 4.f, 4.f);
526         canvas->drawRect(kTile, clear);
527         canvas->drawRect(kTile, outline);
528         shape->redraw(canvas, 1.f);
529         canvas->restore();
530 
531         // Now redraw it into the coverage location (just to the right of the original scale)
532         canvas->save();
533         canvas->translate(x + ShapeRenderer::kTileWidth + 8.f, 4.f);
534         canvas->drawRect(kTile, clear);
535         canvas->drawRect(kTile, outline);
536         shape->redraw(canvas, 1.f, /* debug */ true);
537         canvas->restore();
538 
539         // Lastly, shift the canvas translation down by 8 * kTH + padding for the next set of shapes
540         canvas->translate(0.f, 8.f * ShapeRenderer::kTileHeight + 20.f);
541     }
542 
543     using INHERITED = Sample;
544 };
545 
546 //////////////////////////////////////////////////////////////////////////////
547 
548 DEF_SAMPLE( return new ThinAASample; )
549 
550 }  // namespace skiagm
551