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