1 /*
2  * Copyright 2017 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 "Test.h"
9 
10 #if SK_SUPPORT_GPU
11 
12 #include "GrBackendSemaphore.h"
13 #include "GrClip.h"
14 #include "GrContextPriv.h"
15 #include "GrDefaultGeoProcFactory.h"
16 #include "GrOnFlushResourceProvider.h"
17 #include "GrProxyProvider.h"
18 #include "GrRenderTargetContextPriv.h"
19 #include "GrResourceProvider.h"
20 #include "GrQuad.h"
21 #include "SkBitmap.h"
22 #include "SkPointPriv.h"
23 #include "effects/GrSimpleTextureEffect.h"
24 #include "ops/GrSimpleMeshDrawOpHelper.h"
25 
26 namespace {
27 // This is a simplified mesh drawing op that can be used in the atlas generation test.
28 // Please see AtlasedRectOp below.
29 class NonAARectOp : public GrMeshDrawOp {
30 protected:
31     using Helper = GrSimpleMeshDrawOpHelper;
32 
33 public:
34     DEFINE_OP_CLASS_ID
35 
36     // This creates an instance of a simple non-AA solid color rect-drawing Op
37     static std::unique_ptr<GrDrawOp> Make(GrPaint&& paint, const SkRect& r) {
38         return Helper::FactoryHelper<NonAARectOp>(std::move(paint), r, nullptr, ClassID());
39     }
40 
41     // This creates an instance of a simple non-AA textured rect-drawing Op
42     static std::unique_ptr<GrDrawOp> Make(GrPaint&& paint, const SkRect& r, const SkRect& local) {
43         return Helper::FactoryHelper<NonAARectOp>(std::move(paint), r, &local, ClassID());
44     }
45 
46     GrColor color() const { return fColor; }
47 
48     NonAARectOp(const Helper::MakeArgs& helperArgs, GrColor color, const SkRect& r,
49                 const SkRect* localRect, int32_t classID)
50             : INHERITED(classID)
51             , fColor(color)
52             , fHasLocalRect(SkToBool(localRect))
53             , fRect(r)
54             , fHelper(helperArgs, GrAAType::kNone) {
55         if (fHasLocalRect) {
56             fLocalQuad.set(*localRect);
57         }
58         // Choose some conservative values for aa bloat and zero area.
59         this->setBounds(r, HasAABloat::kYes, IsZeroArea::kYes);
60     }
61 
62     const char* name() const override { return "NonAARectOp"; }
63 
64     void visitProxies(const VisitProxyFunc& func) const override {
65         fHelper.visitProxies(func);
66     }
67 
68     FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; }
69 
70     RequiresDstTexture finalize(const GrCaps& caps, const GrAppliedClip*,
71                                 GrPixelConfigIsClamped dstIsClamped) override {
72         // Set the color to unknown because the subclass may change the color later.
73         GrProcessorAnalysisColor gpColor;
74         gpColor.setToUnknown();
75         // We ignore the clip so pass this rather than the GrAppliedClip param.
76         static GrAppliedClip kNoClip;
77         return fHelper.xpRequiresDstTexture(caps, &kNoClip, dstIsClamped,
78                                             GrProcessorAnalysisCoverage::kNone, &gpColor);
79     }
80 
81 protected:
82     GrColor fColor;
83     bool    fHasLocalRect;
84     GrQuad  fLocalQuad;
85     SkRect  fRect;
86 
87 private:
88     bool onCombineIfPossible(GrOp*, const GrCaps&) override { return false; }
89 
90     void onPrepareDraws(Target* target) override {
91         using namespace GrDefaultGeoProcFactory;
92 
93         // The vertex attrib order is always pos, color, local coords.
94         static const int kColorOffset = sizeof(SkPoint);
95         static const int kLocalOffset = sizeof(SkPoint) + sizeof(GrColor);
96 
97         sk_sp<GrGeometryProcessor> gp =
98                 GrDefaultGeoProcFactory::Make(Color::kPremulGrColorAttribute_Type,
99                                               Coverage::kSolid_Type,
100                                               fHasLocalRect ? LocalCoords::kHasExplicit_Type
101                                                             : LocalCoords::kUnused_Type,
102                                               SkMatrix::I());
103         if (!gp) {
104             SkDebugf("Couldn't create GrGeometryProcessor for GrAtlasedOp\n");
105             return;
106         }
107 
108         size_t vertexStride = gp->getVertexStride();
109 
110         SkASSERT(fHasLocalRect
111                     ? vertexStride == sizeof(GrDefaultGeoProcFactory::PositionColorLocalCoordAttr)
112                     : vertexStride == sizeof(GrDefaultGeoProcFactory::PositionColorAttr));
113 
114         const GrBuffer* indexBuffer;
115         int firstIndex;
116         uint16_t* indices = target->makeIndexSpace(6, &indexBuffer, &firstIndex);
117         if (!indices) {
118             SkDebugf("Indices could not be allocated for GrAtlasedOp.\n");
119             return;
120         }
121 
122         const GrBuffer* vertexBuffer;
123         int firstVertex;
124         void* vertices = target->makeVertexSpace(vertexStride, 4, &vertexBuffer, &firstVertex);
125         if (!vertices) {
126             SkDebugf("Vertices could not be allocated for GrAtlasedOp.\n");
127             return;
128         }
129 
130         // Setup indices
131         indices[0] = 0;
132         indices[1] = 1;
133         indices[2] = 2;
134         indices[3] = 2;
135         indices[4] = 1;
136         indices[5] = 3;
137 
138         // Setup positions
139         SkPoint* position = (SkPoint*) vertices;
140         SkPointPriv::SetRectTriStrip(position, fRect.fLeft, fRect.fTop, fRect.fRight, fRect.fBottom,
141                                   vertexStride);
142 
143         // Setup vertex colors
144         GrColor* color = (GrColor*)((intptr_t)vertices + kColorOffset);
145         for (int i = 0; i < 4; ++i) {
146             *color = fColor;
147             color = (GrColor*)((intptr_t)color + vertexStride);
148         }
149 
150         // Setup local coords
151         if (fHasLocalRect) {
152             SkPoint* coords = (SkPoint*)((intptr_t) vertices + kLocalOffset);
153             for (int i = 0; i < 4; i++) {
154                 *coords = fLocalQuad.point(i);
155                 coords = (SkPoint*)((intptr_t) coords + vertexStride);
156             }
157         }
158 
159         GrMesh mesh(GrPrimitiveType::kTriangles);
160         mesh.setIndexed(indexBuffer, 6, firstIndex, 0, 3);
161         mesh.setVertexData(vertexBuffer, firstVertex);
162 
163         target->draw(gp.get(), fHelper.makePipeline(target), mesh);
164     }
165 
166     Helper fHelper;
167 
168     typedef GrMeshDrawOp INHERITED;
169 };
170 
171 }  // anonymous namespace
172 
173 static constexpr SkRect kEmptyRect = SkRect::MakeEmpty();
174 
175 namespace {
176 
177 /*
178  * Atlased ops just draw themselves as textured rects with the texture pixels being
179  * pulled out of the atlas. Their color is based on their ID.
180  */
181 class AtlasedRectOp final : public NonAARectOp {
182 public:
183     DEFINE_OP_CLASS_ID
184 
185     ~AtlasedRectOp() override {
186         fID = -1;
187     }
188 
189     const char* name() const override { return "AtlasedRectOp"; }
190 
191     int id() const { return fID; }
192 
193     static std::unique_ptr<AtlasedRectOp> Make(GrPaint&& paint, const SkRect& r, int id) {
194         GrDrawOp* op = Helper::FactoryHelper<AtlasedRectOp>(std::move(paint), r, id).release();
195         return std::unique_ptr<AtlasedRectOp>(static_cast<AtlasedRectOp*>(op));
196     }
197 
198     // We set the initial color of the NonAARectOp based on the ID.
199     // Note that we force creation of a NonAARectOp that has local coords in anticipation of
200     // pulling from the atlas.
201     AtlasedRectOp(const Helper::MakeArgs& helperArgs, GrColor color, const SkRect& r, int id)
202             : INHERITED(helperArgs, kColors[id], r, &kEmptyRect, ClassID())
203             , fID(id)
204             , fNext(nullptr) {
205         SkASSERT(fID < kMaxIDs);
206     }
207 
208     void setColor(GrColor color) { fColor = color; }
209     void setLocalRect(const SkRect& localRect) {
210         SkASSERT(fHasLocalRect);    // This should've been created to anticipate this
211         fLocalQuad.set(localRect);
212     }
213 
214     AtlasedRectOp* next() const { return fNext; }
215     void setNext(AtlasedRectOp* next) {
216         fNext = next;
217     }
218 
219 private:
220 
221     static const int kMaxIDs = 9;
222     static const SkColor kColors[kMaxIDs];
223 
224     int            fID;
225     // The Atlased ops have an internal singly-linked list of ops that land in the same opList
226     AtlasedRectOp* fNext;
227 
228     typedef NonAARectOp INHERITED;
229 };
230 
231 }  // anonymous namespace
232 
233 const GrColor AtlasedRectOp::kColors[kMaxIDs] = {
234     GrColorPackRGBA(255, 0, 0, 255),
235     GrColorPackRGBA(0, 255, 0, 255),
236     GrColorPackRGBA(0, 0, 255, 255),
237     GrColorPackRGBA(0, 255, 255, 255),
238     GrColorPackRGBA(255, 0, 255, 255),
239     GrColorPackRGBA(255, 255, 0, 255),
240     GrColorPackRGBA(0, 0, 0, 255),
241     GrColorPackRGBA(128, 128, 128, 255),
242     GrColorPackRGBA(255, 255, 255, 255)
243 };
244 
245 static const int kDrawnTileSize = 16;
246 
247 /*
248  * Rather than performing any rect packing, this atlaser just lays out constant-sized
249  * tiles in an Nx1 row
250  */
251 static const int kAtlasTileSize = 2;
252 
253 /*
254  * This class aggregates the op information required for atlasing
255  */
256 class AtlasObject final : public GrOnFlushCallbackObject {
257 public:
258     AtlasObject() : fDone(false) { }
259 
260     ~AtlasObject() override {
261         SkASSERT(fDone);
262     }
263 
264     void markAsDone() {
265         fDone = true;
266     }
267 
268     // Insert the new op in an internal singly-linked list for 'opListID'
269     void addOp(uint32_t opListID, AtlasedRectOp* op) {
270         LinkedListHeader* header = nullptr;
271         for (int i = 0; i < fOps.count(); ++i) {
272             if (opListID == fOps[i].fID) {
273                 header = &(fOps[i]);
274             }
275         }
276 
277         if (!header) {
278             fOps.push({opListID, nullptr});
279             header = &(fOps[fOps.count()-1]);
280         }
281 
282         op->setNext(header->fHead);
283         header->fHead = op;
284     }
285 
286     // For the time being we need to pre-allocate the atlas.
287     void setAtlasDest(sk_sp<GrTextureProxy> atlasDest) {
288         fAtlasDest = atlasDest;
289     }
290 
291     /*
292      * This callback back creates the atlas and updates the AtlasedRectOps to read from it
293      */
294     void preFlush(GrOnFlushResourceProvider* resourceProvider,
295                   const uint32_t* opListIDs, int numOpListIDs,
296                   SkTArray<sk_sp<GrRenderTargetContext>>* results) override {
297         SkASSERT(!results->count());
298 
299         // Until MDB is landed we will most-likely only have one opList.
300         SkTDArray<LinkedListHeader*> lists;
301         for (int i = 0; i < numOpListIDs; ++i) {
302             if (LinkedListHeader* list = this->getList(opListIDs[i])) {
303                 lists.push(list);
304             }
305         }
306 
307         if (!lists.count()) {
308             return; // nothing to atlas
309         }
310 
311         // TODO: right now we have to pre-allocate the atlas bc the TextureSamplers need a
312         // hard GrTexture
313 #if 0
314         GrSurfaceDesc desc;
315         desc.fFlags = kRenderTarget_GrSurfaceFlag;
316         desc.fWidth = this->numOps() * kAtlasTileSize;
317         desc.fHeight = kAtlasTileSize;
318         desc.fConfig = kRGBA_8888_GrPixelConfig;
319 
320         sk_sp<GrRenderTargetContext> rtc = resourceProvider->makeRenderTargetContext(desc,
321                                                                                      nullptr,
322                                                                                      nullptr);
323 #else
324         // At this point all the GrAtlasedOp's should have lined up to read from 'atlasDest' and
325         // there should either be two writes to clear it or no writes.
326         SkASSERT(9 == fAtlasDest->getPendingReadCnt_TestOnly());
327         SkASSERT(2 == fAtlasDest->getPendingWriteCnt_TestOnly() ||
328                  0 == fAtlasDest->getPendingWriteCnt_TestOnly());
329         sk_sp<GrRenderTargetContext> rtc = resourceProvider->makeRenderTargetContext(
330                                                                            fAtlasDest,
331                                                                            nullptr, nullptr);
332 #endif
333 
334         // clear the atlas
335         rtc->clear(nullptr, 0xFFFFFFFF, GrRenderTargetContext::CanClearFullscreen::kYes);
336 
337         int blocksInAtlas = 0;
338         for (int i = 0; i < lists.count(); ++i) {
339             for (AtlasedRectOp* op = lists[i]->fHead; op; op = op->next()) {
340                 SkIRect r = SkIRect::MakeXYWH(blocksInAtlas*kAtlasTileSize, 0,
341                                               kAtlasTileSize, kAtlasTileSize);
342 
343                 // For now, we avoid the resource buffer issues and just use clears
344 #if 1
345                 rtc->clear(&r, op->color(), GrRenderTargetContext::CanClearFullscreen::kNo);
346 #else
347                 GrPaint paint;
348                 paint.setColor4f(GrColor4f::FromGrColor(op->color()));
349                 std::unique_ptr<GrDrawOp> drawOp(NonAARectOp::Make(std::move(paint),
350                                                                    SkRect::Make(r)));
351                 rtc->priv().testingOnly_addDrawOp(std::move(drawOp));
352 #endif
353                 blocksInAtlas++;
354 
355                 // Set the atlased Op's color to white (so we know we're not using it for
356                 // the final draw).
357                 op->setColor(0xFFFFFFFF);
358 
359                 // Set the atlased Op's localRect to point to where it landed in the atlas
360                 op->setLocalRect(SkRect::Make(r));
361 
362                 // TODO: we also need to set the op's GrSuperDeferredSimpleTextureEffect to point
363                 // to the rtc's proxy!
364             }
365 
366             // We've updated all these ops and we certainly don't want to process them again
367             this->clearOpsFor(lists[i]);
368         }
369 
370         results->push_back(std::move(rtc));
371     }
372 
373 private:
374     typedef struct {
375         uint32_t       fID;
376         AtlasedRectOp* fHead;
377     } LinkedListHeader;
378 
379     LinkedListHeader* getList(uint32_t opListID) {
380         for (int i = 0; i < fOps.count(); ++i) {
381             if (opListID == fOps[i].fID) {
382                 return &(fOps[i]);
383             }
384         }
385         return nullptr;
386     }
387 
388     void clearOpsFor(LinkedListHeader* header) {
389         // The AtlasedRectOps have yet to execute (and this class doesn't own them) so just
390         // forget about them in the laziest way possible.
391         header->fHead = nullptr;
392         header->fID = 0;            // invalid opList ID
393     }
394 
395     // Each opList containing AtlasedRectOps gets its own internal singly-linked list
396     SkTDArray<LinkedListHeader>  fOps;
397 
398     // For the time being we need to pre-allocate the atlas bc the TextureSamplers require
399     // a GrTexture
400     sk_sp<GrTextureProxy>        fAtlasDest;
401 
402     // Set to true when the testing harness expects this object to be no longer used
403     bool                         fDone;
404 };
405 
406 // This creates an off-screen rendertarget whose ops which eventually pull from the atlas.
407 static sk_sp<GrTextureProxy> make_upstream_image(GrContext* context, AtlasObject* object, int start,
408                                                  sk_sp<GrTextureProxy> fakeAtlas) {
409     sk_sp<GrRenderTargetContext> rtc(context->makeDeferredRenderTargetContext(
410                                                                       SkBackingFit::kApprox,
411                                                                       3*kDrawnTileSize,
412                                                                       kDrawnTileSize,
413                                                                       kRGBA_8888_GrPixelConfig,
414                                                                       nullptr));
415 
416     rtc->clear(nullptr, GrColorPackRGBA(255, 0, 0, 255),
417                GrRenderTargetContext::CanClearFullscreen::kYes);
418 
419     for (int i = 0; i < 3; ++i) {
420         SkRect r = SkRect::MakeXYWH(i*kDrawnTileSize, 0, kDrawnTileSize, kDrawnTileSize);
421 
422         auto fp = GrSimpleTextureEffect::Make(fakeAtlas, SkMatrix::I());
423         GrPaint paint;
424         paint.addColorFragmentProcessor(std::move(fp));
425         paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
426         std::unique_ptr<AtlasedRectOp> op(AtlasedRectOp::Make(std::move(paint), r, start + i));
427 
428         AtlasedRectOp* sparePtr = op.get();
429 
430         uint32_t opListID = rtc->priv().testingOnly_addDrawOp(std::move(op));
431 
432         object->addOp(opListID, sparePtr);
433     }
434 
435     return rtc->asTextureProxyRef();
436 }
437 
438 // Enable this if you want to debug the final draws w/o having the atlasCallback create the
439 // atlas
440 #if 0
441 #include "SkImageEncoder.h"
442 #include "SkGrPriv.h"
443 #include "sk_tool_utils.h"
444 
445 static void save_bm(const SkBitmap& bm, const char name[]) {
446     bool result = sk_tool_utils::EncodeImageToFile(name, bm, SkEncodedImageFormat::kPNG, 100);
447     SkASSERT(result);
448 }
449 
450 sk_sp<GrTextureProxy> pre_create_atlas(GrContext* context) {
451     SkBitmap bm;
452     bm.allocN32Pixels(18, 2, true);
453     bm.erase(SK_ColorRED,     SkIRect::MakeXYWH(0, 0, 2, 2));
454     bm.erase(SK_ColorGREEN,   SkIRect::MakeXYWH(2, 0, 2, 2));
455     bm.erase(SK_ColorBLUE,    SkIRect::MakeXYWH(4, 0, 2, 2));
456     bm.erase(SK_ColorCYAN,    SkIRect::MakeXYWH(6, 0, 2, 2));
457     bm.erase(SK_ColorMAGENTA, SkIRect::MakeXYWH(8, 0, 2, 2));
458     bm.erase(SK_ColorYELLOW,  SkIRect::MakeXYWH(10, 0, 2, 2));
459     bm.erase(SK_ColorBLACK,   SkIRect::MakeXYWH(12, 0, 2, 2));
460     bm.erase(SK_ColorGRAY,    SkIRect::MakeXYWH(14, 0, 2, 2));
461     bm.erase(SK_ColorWHITE,   SkIRect::MakeXYWH(16, 0, 2, 2));
462 
463 #if 1
464     save_bm(bm, "atlas-fake.png");
465 #endif
466 
467     GrSurfaceDesc desc = GrImageInfoToSurfaceDesc(bm.info(), *context->caps());
468     desc.fFlags |= kRenderTarget_GrSurfaceFlag;
469 
470     sk_sp<GrSurfaceProxy> tmp = GrSurfaceProxy::MakeDeferred(*context->caps(),
471                                                              context->textureProvider(),
472                                                              desc, SkBudgeted::kYes,
473                                                              bm.getPixels(), bm.rowBytes());
474 
475     return sk_ref_sp(tmp->asTextureProxy());
476 }
477 #else
478 // TODO: this is unfortunate and must be removed. We want the atlas to be created later.
479 sk_sp<GrTextureProxy> pre_create_atlas(GrProxyProvider* proxyProvider) {
480     GrSurfaceDesc desc;
481     desc.fFlags = kRenderTarget_GrSurfaceFlag;
482     desc.fOrigin = kBottomLeft_GrSurfaceOrigin;
483     desc.fWidth = 32;
484     desc.fHeight = 16;
485     desc.fConfig = kSkia8888_GrPixelConfig;
486 
487     return proxyProvider->createProxy(desc, SkBackingFit::kExact, SkBudgeted::kYes,
488                                       GrResourceProvider::kNoPendingIO_Flag);
489 }
490 #endif
491 
492 static void test_color(skiatest::Reporter* reporter, const SkBitmap& bm, int x, SkColor expected) {
493     SkColor readback = bm.getColor(x, kDrawnTileSize/2);
494     REPORTER_ASSERT(reporter, expected == readback);
495     if (expected != readback) {
496         SkDebugf("Color mismatch: %x %x\n", expected, readback);
497     }
498 }
499 
500 /*
501  * For the atlasing test we make a DAG that looks like:
502  *
503  *    RT1 with ops: 0,1,2       RT2 with ops: 3,4,5       RT3 with ops: 6,7,8
504  *                     \         /
505  *                      \       /
506  *                         RT4
507  * We then flush RT4 and expect only ops 0-5 to be atlased together.
508  * Each op is just a solid colored rect so both the atlas and the final image should appear as:
509  *           R G B C M Y
510  * with the atlas having width = 6*kAtlasTileSize and height = kAtlasTileSize.
511  *
512  * Note: until MDB lands, the atlas will actually have width= 9*kAtlasTileSize and look like:
513  *           R G B C M Y K Grey White
514  */
515 DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(OnFlushCallbackTest, reporter, ctxInfo) {
516     static const int kNumProxies = 3;
517 
518     GrContext* context = ctxInfo.grContext();
519 
520     AtlasObject object;
521 
522     // For now (until we add a GrSuperDeferredSimpleTextureEffect), we create the final atlas
523     // proxy ahead of time.
524     sk_sp<GrTextureProxy> atlasDest = pre_create_atlas(context->contextPriv().proxyProvider());
525 
526     object.setAtlasDest(atlasDest);
527 
528     context->contextPriv().addOnFlushCallbackObject(&object);
529 
530     sk_sp<GrTextureProxy> proxies[kNumProxies];
531     for (int i = 0; i < kNumProxies; ++i) {
532         proxies[i] = make_upstream_image(context, &object, i*3, atlasDest);
533     }
534 
535     static const int kFinalWidth = 6*kDrawnTileSize;
536     static const int kFinalHeight = kDrawnTileSize;
537 
538     sk_sp<GrRenderTargetContext> rtc(context->makeDeferredRenderTargetContext(
539                                                                       SkBackingFit::kApprox,
540                                                                       kFinalWidth,
541                                                                       kFinalHeight,
542                                                                       kRGBA_8888_GrPixelConfig,
543                                                                       nullptr));
544 
545     rtc->clear(nullptr, 0xFFFFFFFF, GrRenderTargetContext::CanClearFullscreen::kYes);
546 
547     // Note that this doesn't include the third texture proxy
548     for (int i = 0; i < kNumProxies-1; ++i) {
549         SkRect r = SkRect::MakeXYWH(i*3*kDrawnTileSize, 0, 3*kDrawnTileSize, kDrawnTileSize);
550 
551         SkMatrix t = SkMatrix::MakeTrans(-i*3*kDrawnTileSize, 0);
552 
553         GrPaint paint;
554         auto fp = GrSimpleTextureEffect::Make(std::move(proxies[i]), t);
555         paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
556         paint.addColorFragmentProcessor(std::move(fp));
557 
558         rtc->drawRect(GrNoClip(), std::move(paint), GrAA::kNo, SkMatrix::I(), r);
559     }
560 
561     rtc->prepareForExternalIO(0, nullptr);
562 
563     SkBitmap readBack;
564     readBack.allocN32Pixels(kFinalWidth, kFinalHeight);
565 
566     SkDEBUGCODE(bool result =) rtc->readPixels(readBack.info(), readBack.getPixels(),
567                                                readBack.rowBytes(), 0, 0);
568     SkASSERT(result);
569 
570     context->contextPriv().testingOnly_flushAndRemoveOnFlushCallbackObject(&object);
571 
572     object.markAsDone();
573 
574     int x = kDrawnTileSize/2;
575     test_color(reporter, readBack, x, SK_ColorRED);
576     x += kDrawnTileSize;
577     test_color(reporter, readBack, x, SK_ColorGREEN);
578     x += kDrawnTileSize;
579     test_color(reporter, readBack, x, SK_ColorBLUE);
580     x += kDrawnTileSize;
581     test_color(reporter, readBack, x, SK_ColorCYAN);
582     x += kDrawnTileSize;
583     test_color(reporter, readBack, x, SK_ColorMAGENTA);
584     x += kDrawnTileSize;
585     test_color(reporter, readBack, x, SK_ColorYELLOW);
586 }
587 
588 #endif
589