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