beziereffects.cpp revision b493eebca14aefbd5f22fb5d45ba978b19db4b18
1/*
2 * Copyright 2013 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// This test only works with the GPU backend.
9
10#include "gm.h"
11
12#if SK_SUPPORT_GPU
13
14#include "GrContext.h"
15#include "GrOpFlushState.h"
16#include "GrPathUtils.h"
17#include "GrRenderTargetContextPriv.h"
18#include "GrTest.h"
19#include "SkColorPriv.h"
20#include "SkGeometry.h"
21#include "effects/GrBezierEffect.h"
22#include "ops/GrMeshDrawOp.h"
23
24namespace skiagm {
25
26class BezierTestOp : public GrMeshDrawOp {
27public:
28    FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; }
29
30    RequiresDstTexture finalize(const GrCaps& caps, const GrAppliedClip* clip) override {
31        auto analysis = fProcessorSet.finalize(fColor, GrProcessorAnalysisCoverage::kSingleChannel,
32                                               clip, false, caps, &fColor);
33        return analysis.requiresDstTexture() ? RequiresDstTexture::kYes : RequiresDstTexture::kNo;
34    }
35
36    void visitProxies(VisitProxyFunc func) const override {
37        fProcessorSet.visitProxies(func);
38    }
39
40protected:
41    BezierTestOp(sk_sp<GrGeometryProcessor> gp, const SkRect& rect, GrColor color, int32_t classID)
42            : INHERITED(classID)
43            , fRect(rect)
44            , fColor(color)
45            , fGeometryProcessor(std::move(gp))
46            , fProcessorSet(SkBlendMode::kSrc) {
47        this->setBounds(rect, HasAABloat::kYes, IsZeroArea::kNo);
48    }
49
50    const GrPipeline* makePipeline(Target* target) {
51        return target->makePipeline(0, std::move(fProcessorSet), target->detachAppliedClip());
52    }
53
54    const GrGeometryProcessor* gp() const { return fGeometryProcessor.get(); }
55
56    const SkRect& rect() const { return fRect; }
57    GrColor color() const { return fColor; }
58
59private:
60    bool onCombineIfPossible(GrOp* op, const GrCaps& caps) override { return false; }
61
62    SkRect fRect;
63    GrColor fColor;
64    sk_sp<GrGeometryProcessor> fGeometryProcessor;
65    GrProcessorSet fProcessorSet;
66
67    typedef GrMeshDrawOp INHERITED;
68};
69
70class BezierCubicTestOp : public BezierTestOp {
71public:
72    DEFINE_OP_CLASS_ID
73
74    const char* name() const override { return "BezierCubicTestOp"; }
75
76    static std::unique_ptr<GrDrawOp> Make(sk_sp<GrGeometryProcessor> gp, const SkRect& rect,
77                                          GrColor color) {
78        return std::unique_ptr<GrDrawOp>(new BezierCubicTestOp(std::move(gp), rect, color));
79    }
80
81private:
82    BezierCubicTestOp(sk_sp<GrGeometryProcessor> gp, const SkRect& rect, GrColor color)
83            : INHERITED(std::move(gp), rect, color, ClassID()) {}
84
85    void onPrepareDraws(Target* target) override {
86        QuadHelper helper;
87        size_t vertexStride = this->gp()->getVertexStride();
88        SkASSERT(vertexStride == sizeof(SkPoint));
89        SkPoint* pts = reinterpret_cast<SkPoint*>(helper.init(target, vertexStride, 1));
90        if (!pts) {
91            return;
92        }
93        SkRect rect = this->rect();
94        pts[0].setRectFan(rect.fLeft, rect.fTop, rect.fRight, rect.fBottom, vertexStride);
95        helper.recordDraw(target, this->gp(), this->makePipeline(target));
96    }
97
98    static constexpr int kVertsPerCubic = 4;
99    static constexpr int kIndicesPerCubic = 6;
100
101    typedef BezierTestOp INHERITED;
102};
103
104/**
105 * This GM directly exercises effects that draw Bezier curves in the GPU backend.
106 */
107class BezierCubicEffects : public GM {
108public:
109    BezierCubicEffects() {
110        this->setBGColor(0xFFFFFFFF);
111    }
112
113protected:
114    SkString onShortName() override {
115        return SkString("bezier_cubic_effects");
116    }
117
118    SkISize onISize() override {
119        return SkISize::Make(800, 800);
120    }
121
122    void onDraw(SkCanvas* canvas) override {
123        GrRenderTargetContext* renderTargetContext =
124            canvas->internal_private_accessTopLayerRenderTargetContext();
125        if (!renderTargetContext) {
126            skiagm::GM::DrawGpuOnlyMessage(canvas);
127            return;
128        }
129
130        GrContext* context = canvas->getGrContext();
131        if (!context) {
132            return;
133        }
134
135        struct Vertex {
136            SkPoint fPosition;
137            float   fKLM[4]; // The last value is ignored. The effect expects a vec4f.
138        };
139
140        constexpr int kNumCubics = 15;
141        SkRandom rand;
142
143        // Mult by 3 for each edge effect type
144        int numCols = SkScalarCeilToInt(SkScalarSqrt(SkIntToScalar(kNumCubics*3)));
145        int numRows = SkScalarCeilToInt(SkIntToScalar(kNumCubics*3) / numCols);
146        SkScalar w = SkIntToScalar(renderTargetContext->width()) / numCols;
147        SkScalar h = SkIntToScalar(renderTargetContext->height()) / numRows;
148        int row = 0;
149        int col = 0;
150        constexpr GrColor color = 0xff000000;
151
152        for (int i = 0; i < kNumCubics; ++i) {
153            SkPoint baseControlPts[] = {
154                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
155                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
156                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
157                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)}
158            };
159            for(GrPrimitiveEdgeType edgeType : {kFillBW_GrProcessorEdgeType,
160                                                kFillAA_GrProcessorEdgeType,
161                                                kHairlineAA_GrProcessorEdgeType}) {
162                SkScalar x = col * w;
163                SkScalar y = row * h;
164                SkPoint controlPts[] = {
165                    {x + baseControlPts[0].fX, y + baseControlPts[0].fY},
166                    {x + baseControlPts[1].fX, y + baseControlPts[1].fY},
167                    {x + baseControlPts[2].fX, y + baseControlPts[2].fY},
168                    {x + baseControlPts[3].fX, y + baseControlPts[3].fY}
169                };
170                SkPoint chopped[10];
171                SkMatrix klm;
172                int loopIndex;
173                int cnt = GrPathUtils::chopCubicAtLoopIntersection(controlPts,
174                                                                   chopped,
175                                                                   &klm,
176                                                                   &loopIndex);
177
178                SkPaint ctrlPtPaint;
179                ctrlPtPaint.setColor(rand.nextU() | 0xFF000000);
180                canvas->drawCircle(controlPts[0], 8.f, ctrlPtPaint);
181                for (int i = 1; i < 4; ++i) {
182                    canvas->drawCircle(controlPts[i], 6.f, ctrlPtPaint);
183                }
184
185                SkPaint polyPaint;
186                polyPaint.setColor(0xffA0A0A0);
187                polyPaint.setStrokeWidth(0);
188                polyPaint.setStyle(SkPaint::kStroke_Style);
189                canvas->drawPoints(SkCanvas::kPolygon_PointMode, 4, controlPts, polyPaint);
190
191                SkPaint choppedPtPaint;
192                choppedPtPaint.setColor(~ctrlPtPaint.getColor() | 0xFF000000);
193
194                for (int c = 0; c < cnt; ++c) {
195                    SkPoint* pts = chopped + 3 * c;
196
197                    for (int i = 0; i < 4; ++i) {
198                        canvas->drawCircle(pts[i], 3.f, choppedPtPaint);
199                    }
200
201                    SkRect bounds;
202                    bounds.set(pts, 4);
203
204                    SkPaint boundsPaint;
205                    boundsPaint.setColor(0xff808080);
206                    boundsPaint.setStrokeWidth(0);
207                    boundsPaint.setStyle(SkPaint::kStroke_Style);
208                    canvas->drawRect(bounds, boundsPaint);
209
210
211                    bool flipKL = (c == loopIndex && cnt != 3);
212                    sk_sp<GrGeometryProcessor> gp = GrCubicEffect::Make(color, SkMatrix::I(), klm,
213                                                                        flipKL, edgeType,
214                                                                        *context->caps());
215                    if (!gp) {
216                        break;
217                    }
218
219                    std::unique_ptr<GrDrawOp> op =
220                            BezierCubicTestOp::Make(std::move(gp), bounds, color);
221                    renderTargetContext->priv().testingOnly_addDrawOp(std::move(op));
222                }
223                ++col;
224                if (numCols == col) {
225                    col = 0;
226                    ++row;
227                }
228            }
229        }
230    }
231
232private:
233    typedef GM INHERITED;
234};
235
236//////////////////////////////////////////////////////////////////////////////
237
238class BezierConicTestOp : public BezierTestOp {
239public:
240    DEFINE_OP_CLASS_ID
241
242    const char* name() const override { return "BezierConicTestOp"; }
243
244    static std::unique_ptr<GrDrawOp> Make(sk_sp<GrGeometryProcessor> gp, const SkRect& rect,
245                                          GrColor color, const SkMatrix& klm) {
246        return std::unique_ptr<GrMeshDrawOp>(
247                new BezierConicTestOp(std::move(gp), rect, color, klm));
248    }
249
250private:
251    BezierConicTestOp(sk_sp<GrGeometryProcessor> gp, const SkRect& rect, GrColor color,
252                      const SkMatrix& klm)
253            : INHERITED(std::move(gp), rect, color, ClassID()), fKLM(klm) {}
254
255    struct Vertex {
256        SkPoint fPosition;
257        float   fKLM[4]; // The last value is ignored. The effect expects a vec4f.
258    };
259
260    void onPrepareDraws(Target* target) override {
261        QuadHelper helper;
262        size_t vertexStride = this->gp()->getVertexStride();
263        SkASSERT(vertexStride == sizeof(Vertex));
264        Vertex* verts = reinterpret_cast<Vertex*>(helper.init(target, vertexStride, 1));
265        if (!verts) {
266            return;
267        }
268        SkRect rect = this->rect();
269        verts[0].fPosition.setRectFan(rect.fLeft, rect.fTop, rect.fRight, rect.fBottom,
270                                      sizeof(Vertex));
271        for (int v = 0; v < 4; ++v) {
272            SkScalar pt3[3] = {verts[v].fPosition.x(), verts[v].fPosition.y(), 1.f};
273            fKLM.mapHomogeneousPoints(verts[v].fKLM, pt3, 1);
274        }
275        helper.recordDraw(target, this->gp(), this->makePipeline(target));
276    }
277
278    SkMatrix fKLM;
279
280    static constexpr int kVertsPerCubic = 4;
281    static constexpr int kIndicesPerCubic = 6;
282
283    typedef BezierTestOp INHERITED;
284};
285
286
287/**
288 * This GM directly exercises effects that draw Bezier curves in the GPU backend.
289 */
290class BezierConicEffects : public GM {
291public:
292    BezierConicEffects() {
293        this->setBGColor(0xFFFFFFFF);
294    }
295
296protected:
297    SkString onShortName() override {
298        return SkString("bezier_conic_effects");
299    }
300
301    SkISize onISize() override {
302        return SkISize::Make(800, 800);
303    }
304
305
306    void onDraw(SkCanvas* canvas) override {
307        GrRenderTargetContext* renderTargetContext =
308            canvas->internal_private_accessTopLayerRenderTargetContext();
309        if (!renderTargetContext) {
310            skiagm::GM::DrawGpuOnlyMessage(canvas);
311            return;
312        }
313
314        GrContext* context = canvas->getGrContext();
315        if (!context) {
316            return;
317        }
318
319        struct Vertex {
320            SkPoint fPosition;
321            float   fKLM[4]; // The last value is ignored. The effect expects a vec4f.
322        };
323
324        constexpr int kNumConics = 10;
325        SkRandom rand;
326
327        // Mult by 3 for each edge effect type
328        int numCols = SkScalarCeilToInt(SkScalarSqrt(SkIntToScalar(kNumConics*3)));
329        int numRows = SkScalarCeilToInt(SkIntToScalar(kNumConics*3) / numCols);
330        SkScalar w = SkIntToScalar(renderTargetContext->width()) / numCols;
331        SkScalar h = SkIntToScalar(renderTargetContext->height()) / numRows;
332        int row = 0;
333        int col = 0;
334        constexpr GrColor color = 0xff000000;
335
336        for (int i = 0; i < kNumConics; ++i) {
337            SkPoint baseControlPts[] = {
338                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
339                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
340                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)}
341            };
342            SkScalar weight = rand.nextRangeF(0.f, 2.f);
343            for(int edgeType = 0; edgeType < kGrProcessorEdgeTypeCnt; ++edgeType) {
344                sk_sp<GrGeometryProcessor> gp;
345                GrPrimitiveEdgeType et = (GrPrimitiveEdgeType)edgeType;
346                gp = GrConicEffect::Make(color, SkMatrix::I(), et,
347                                         *context->caps(), SkMatrix::I(), false);
348                if (!gp) {
349                    continue;
350                }
351
352                SkScalar x = col * w;
353                SkScalar y = row * h;
354                SkPoint controlPts[] = {
355                    {x + baseControlPts[0].fX, y + baseControlPts[0].fY},
356                    {x + baseControlPts[1].fX, y + baseControlPts[1].fY},
357                    {x + baseControlPts[2].fX, y + baseControlPts[2].fY}
358                };
359                SkConic dst[4];
360                SkMatrix klm;
361                int cnt = chop_conic(controlPts, dst, weight);
362                GrPathUtils::getConicKLM(controlPts, weight, &klm);
363
364                SkPaint ctrlPtPaint;
365                ctrlPtPaint.setColor(rand.nextU() | 0xFF000000);
366                for (int i = 0; i < 3; ++i) {
367                    canvas->drawCircle(controlPts[i], 6.f, ctrlPtPaint);
368                }
369
370                SkPaint polyPaint;
371                polyPaint.setColor(0xffA0A0A0);
372                polyPaint.setStrokeWidth(0);
373                polyPaint.setStyle(SkPaint::kStroke_Style);
374                canvas->drawPoints(SkCanvas::kPolygon_PointMode, 3, controlPts, polyPaint);
375
376                SkPaint choppedPtPaint;
377                choppedPtPaint.setColor(~ctrlPtPaint.getColor() | 0xFF000000);
378
379                for (int c = 0; c < cnt; ++c) {
380                    SkPoint* pts = dst[c].fPts;
381                    for (int i = 0; i < 3; ++i) {
382                        canvas->drawCircle(pts[i], 3.f, choppedPtPaint);
383                    }
384
385                    SkRect bounds;
386                    //SkPoint bPts[] = {{0.f, 0.f}, {800.f, 800.f}};
387                    //bounds.set(bPts, 2);
388                    bounds.set(pts, 3);
389
390                    SkPaint boundsPaint;
391                    boundsPaint.setColor(0xff808080);
392                    boundsPaint.setStrokeWidth(0);
393                    boundsPaint.setStyle(SkPaint::kStroke_Style);
394                    canvas->drawRect(bounds, boundsPaint);
395
396                    std::unique_ptr<GrDrawOp> op = BezierConicTestOp::Make(gp, bounds, color, klm);
397                    renderTargetContext->priv().testingOnly_addDrawOp(std::move(op));
398                }
399                ++col;
400                if (numCols == col) {
401                    col = 0;
402                    ++row;
403                }
404            }
405        }
406    }
407
408private:
409    // Uses the max curvature function for quads to estimate
410    // where to chop the conic. If the max curvature is not
411    // found along the curve segment it will return 1 and
412    // dst[0] is the original conic. If it returns 2 the dst[0]
413    // and dst[1] are the two new conics.
414    int split_conic(const SkPoint src[3], SkConic dst[2], const SkScalar weight) {
415        SkScalar t = SkFindQuadMaxCurvature(src);
416        if (t == 0) {
417            if (dst) {
418                dst[0].set(src, weight);
419            }
420            return 1;
421        } else {
422            if (dst) {
423                SkConic conic;
424                conic.set(src, weight);
425                if (!conic.chopAt(t, dst)) {
426                    dst[0].set(src, weight);
427                    return 1;
428                }
429            }
430            return 2;
431        }
432    }
433
434    // Calls split_conic on the entire conic and then once more on each subsection.
435    // Most cases will result in either 1 conic (chop point is not within t range)
436    // or 3 points (split once and then one subsection is split again).
437    int chop_conic(const SkPoint src[3], SkConic dst[4], const SkScalar weight) {
438        SkConic dstTemp[2];
439        int conicCnt = split_conic(src, dstTemp, weight);
440        if (2 == conicCnt) {
441            int conicCnt2 = split_conic(dstTemp[0].fPts, dst, dstTemp[0].fW);
442            conicCnt = conicCnt2 + split_conic(dstTemp[1].fPts, &dst[conicCnt2], dstTemp[1].fW);
443        } else {
444            dst[0] = dstTemp[0];
445        }
446        return conicCnt;
447    }
448
449    typedef GM INHERITED;
450};
451
452//////////////////////////////////////////////////////////////////////////////
453
454class BezierQuadTestOp : public BezierTestOp {
455public:
456    DEFINE_OP_CLASS_ID
457    const char* name() const override { return "BezierQuadTestOp"; }
458
459    static std::unique_ptr<GrDrawOp> Make(sk_sp<GrGeometryProcessor> gp, const SkRect& rect,
460                                          GrColor color, const GrPathUtils::QuadUVMatrix& devToUV) {
461        return std::unique_ptr<GrDrawOp>(new BezierQuadTestOp(std::move(gp), rect, color, devToUV));
462    }
463
464private:
465    BezierQuadTestOp(sk_sp<GrGeometryProcessor> gp, const SkRect& rect, GrColor color,
466                     const GrPathUtils::QuadUVMatrix& devToUV)
467            : INHERITED(std::move(gp), rect, color, ClassID()), fDevToUV(devToUV) {}
468
469    struct Vertex {
470        SkPoint fPosition;
471        float   fKLM[4]; // The last value is ignored. The effect expects a vec4f.
472    };
473
474    void onPrepareDraws(Target* target) override {
475        QuadHelper helper;
476        size_t vertexStride = this->gp()->getVertexStride();
477        SkASSERT(vertexStride == sizeof(Vertex));
478        Vertex* verts = reinterpret_cast<Vertex*>(helper.init(target, vertexStride, 1));
479        if (!verts) {
480            return;
481        }
482        SkRect rect = this->rect();
483        verts[0].fPosition.setRectFan(rect.fLeft, rect.fTop, rect.fRight, rect.fBottom,
484                                      sizeof(Vertex));
485        fDevToUV.apply<4, sizeof(Vertex), sizeof(SkPoint)>(verts);
486        helper.recordDraw(target, this->gp(), this->makePipeline(target));
487    }
488
489    GrPathUtils::QuadUVMatrix fDevToUV;
490
491    static constexpr int kVertsPerCubic = 4;
492    static constexpr int kIndicesPerCubic = 6;
493
494    typedef BezierTestOp INHERITED;
495};
496
497/**
498 * This GM directly exercises effects that draw Bezier quad curves in the GPU backend.
499 */
500class BezierQuadEffects : public GM {
501public:
502    BezierQuadEffects() {
503        this->setBGColor(0xFFFFFFFF);
504    }
505
506protected:
507    SkString onShortName() override {
508        return SkString("bezier_quad_effects");
509    }
510
511    SkISize onISize() override {
512        return SkISize::Make(800, 800);
513    }
514
515
516    void onDraw(SkCanvas* canvas) override {
517        GrRenderTargetContext* renderTargetContext =
518            canvas->internal_private_accessTopLayerRenderTargetContext();
519        if (!renderTargetContext) {
520            skiagm::GM::DrawGpuOnlyMessage(canvas);
521            return;
522        }
523
524        GrContext* context = canvas->getGrContext();
525        if (!context) {
526            return;
527        }
528
529        struct Vertex {
530            SkPoint fPosition;
531            float   fUV[4]; // The last two values are ignored. The effect expects a vec4f.
532        };
533
534        constexpr int kNumQuads = 5;
535        SkRandom rand;
536
537        int numCols = SkScalarCeilToInt(SkScalarSqrt(SkIntToScalar(kNumQuads*3)));
538        int numRows = SkScalarCeilToInt(SkIntToScalar(kNumQuads*3) / numCols);
539        SkScalar w = SkIntToScalar(renderTargetContext->width()) / numCols;
540        SkScalar h = SkIntToScalar(renderTargetContext->height()) / numRows;
541        int row = 0;
542        int col = 0;
543        constexpr GrColor color = 0xff000000;
544
545        for (int i = 0; i < kNumQuads; ++i) {
546            SkPoint baseControlPts[] = {
547                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
548                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)},
549                {rand.nextRangeF(0.f, w), rand.nextRangeF(0.f, h)}
550            };
551            for(int edgeType = 0; edgeType < kGrProcessorEdgeTypeCnt; ++edgeType) {
552                sk_sp<GrGeometryProcessor> gp;
553                GrPrimitiveEdgeType et = (GrPrimitiveEdgeType)edgeType;
554                gp = GrQuadEffect::Make(color, SkMatrix::I(), et,
555                                        *context->caps(), SkMatrix::I(), false);
556                if (!gp) {
557                    continue;
558                }
559
560                SkScalar x = col * w;
561                SkScalar y = row * h;
562                SkPoint controlPts[] = {
563                    {x + baseControlPts[0].fX, y + baseControlPts[0].fY},
564                    {x + baseControlPts[1].fX, y + baseControlPts[1].fY},
565                    {x + baseControlPts[2].fX, y + baseControlPts[2].fY}
566                };
567                SkPoint chopped[5];
568                int cnt = SkChopQuadAtMaxCurvature(controlPts, chopped);
569
570                SkPaint ctrlPtPaint;
571                ctrlPtPaint.setColor(rand.nextU() | 0xFF000000);
572                for (int i = 0; i < 3; ++i) {
573                    canvas->drawCircle(controlPts[i], 6.f, ctrlPtPaint);
574                }
575
576                SkPaint polyPaint;
577                polyPaint.setColor(0xffA0A0A0);
578                polyPaint.setStrokeWidth(0);
579                polyPaint.setStyle(SkPaint::kStroke_Style);
580                canvas->drawPoints(SkCanvas::kPolygon_PointMode, 3, controlPts, polyPaint);
581
582                SkPaint choppedPtPaint;
583                choppedPtPaint.setColor(~ctrlPtPaint.getColor() | 0xFF000000);
584
585                for (int c = 0; c < cnt; ++c) {
586                    SkPoint* pts = chopped + 2 * c;
587
588                    for (int i = 0; i < 3; ++i) {
589                        canvas->drawCircle(pts[i], 3.f, choppedPtPaint);
590                    }
591
592                    SkRect bounds;
593                    bounds.set(pts, 3);
594
595                    SkPaint boundsPaint;
596                    boundsPaint.setColor(0xff808080);
597                    boundsPaint.setStrokeWidth(0);
598                    boundsPaint.setStyle(SkPaint::kStroke_Style);
599                    canvas->drawRect(bounds, boundsPaint);
600
601                    GrPaint grPaint;
602                    grPaint.setXPFactory(GrPorterDuffXPFactory::Get(SkBlendMode::kSrc));
603
604                    GrPathUtils::QuadUVMatrix DevToUV(pts);
605
606                    std::unique_ptr<GrDrawOp> op =
607                            BezierQuadTestOp::Make(gp, bounds, color, DevToUV);
608                    renderTargetContext->priv().testingOnly_addDrawOp(std::move(op));
609                }
610                ++col;
611                if (numCols == col) {
612                    col = 0;
613                    ++row;
614                }
615            }
616        }
617    }
618
619private:
620    typedef GM INHERITED;
621};
622
623DEF_GM(return new BezierCubicEffects;)
624DEF_GM(return new BezierConicEffects;)
625DEF_GM(return new BezierQuadEffects;)
626}
627
628#endif
629