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