PictureRenderer.cpp revision a9e3a369c18c6d5f41724e837e3ba0fa87d8c559
1/*
2 * Copyright 2012 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 "PictureRenderer.h"
9#include "picture_utils.h"
10#include "SamplePipeControllers.h"
11#include "SkCanvas.h"
12#include "SkDevice.h"
13#include "SkGPipe.h"
14#if SK_SUPPORT_GPU
15#include "SkGpuDevice.h"
16#endif
17#include "SkGraphics.h"
18#include "SkImageEncoder.h"
19#include "SkMaskFilter.h"
20#include "SkMatrix.h"
21#include "SkPicture.h"
22#include "SkRTree.h"
23#include "SkScalar.h"
24#include "SkStream.h"
25#include "SkString.h"
26#include "SkTemplates.h"
27#include "SkTileGrid.h"
28#include "SkTDArray.h"
29#include "SkThreadUtils.h"
30#include "SkTypes.h"
31
32namespace sk_tools {
33
34enum {
35    kDefaultTileWidth = 256,
36    kDefaultTileHeight = 256
37};
38
39void PictureRenderer::init(SkPicture* pict) {
40    SkASSERT(NULL == fPicture);
41    SkASSERT(NULL == fCanvas.get());
42    if (fPicture != NULL || NULL != fCanvas.get()) {
43        return;
44    }
45
46    SkASSERT(pict != NULL);
47    if (NULL == pict) {
48        return;
49    }
50
51    fPicture = pict;
52    fPicture->ref();
53    fCanvas.reset(this->setupCanvas());
54}
55
56class FlagsDrawFilter : public SkDrawFilter {
57public:
58    FlagsDrawFilter(PictureRenderer::DrawFilterFlags* flags) :
59        fFlags(flags) {}
60
61    virtual void filter(SkPaint* paint, Type t) {
62        paint->setFlags(paint->getFlags() & ~fFlags[t] & SkPaint::kAllFlags);
63        if ((PictureRenderer::kBlur_DrawFilterFlag | PictureRenderer::kLowBlur_DrawFilterFlag)
64                & fFlags[t]) {
65            SkMaskFilter* maskFilter = paint->getMaskFilter();
66            SkMaskFilter::BlurInfo blurInfo;
67            if (maskFilter && maskFilter->asABlur(&blurInfo)) {
68                if (PictureRenderer::kBlur_DrawFilterFlag & fFlags[t]) {
69                    paint->setMaskFilter(NULL);
70                } else {
71                    blurInfo.fHighQuality = false;
72                    maskFilter->setAsABlur(blurInfo);
73                }
74            }
75        }
76        if (PictureRenderer::kHinting_DrawFilterFlag & fFlags[t]) {
77            paint->setHinting(SkPaint::kNo_Hinting);
78        } else if (PictureRenderer::kSlightHinting_DrawFilterFlag & fFlags[t]) {
79            paint->setHinting(SkPaint::kSlight_Hinting);
80        }
81    }
82
83private:
84    PictureRenderer::DrawFilterFlags* fFlags;
85};
86
87static SkCanvas* setUpFilter(SkCanvas* canvas, PictureRenderer::DrawFilterFlags* drawFilters) {
88    if (drawFilters && !canvas->getDrawFilter()) {
89        canvas->setDrawFilter(SkNEW_ARGS(FlagsDrawFilter, (drawFilters)))->unref();
90        if (drawFilters[0] & PictureRenderer::kAAClip_DrawFilterFlag) {
91            canvas->setAllowSoftClip(false);
92        }
93    }
94    return canvas;
95}
96
97SkCanvas* PictureRenderer::setupCanvas() {
98    return this->setupCanvas(fPicture->width(), fPicture->height());
99}
100
101SkCanvas* PictureRenderer::setupCanvas(int width, int height) {
102    SkCanvas* canvas;
103    switch(fDeviceType) {
104        case kBitmap_DeviceType: {
105            SkBitmap bitmap;
106            sk_tools::setup_bitmap(&bitmap, width, height);
107            canvas = SkNEW_ARGS(SkCanvas, (bitmap));
108            return setUpFilter(canvas, fDrawFilters);
109        }
110#if SK_SUPPORT_GPU
111        case kGPU_DeviceType: {
112            SkAutoTUnref<SkGpuDevice> device(SkNEW_ARGS(SkGpuDevice,
113                                                    (fGrContext, SkBitmap::kARGB_8888_Config,
114                                                    width, height)));
115            canvas = SkNEW_ARGS(SkCanvas, (device.get()));
116            return setUpFilter(canvas, fDrawFilters);
117        }
118#endif
119        default:
120            SkASSERT(0);
121    }
122
123    return NULL;
124}
125
126void PictureRenderer::end() {
127    this->resetState();
128    SkSafeUnref(fPicture);
129    fPicture = NULL;
130    fCanvas.reset(NULL);
131}
132
133/** Converts fPicture to a picture that uses a BBoxHierarchy.
134 *  PictureRenderer subclasses that are used to test picture playback
135 *  should call this method during init.
136 */
137void PictureRenderer::buildBBoxHierarchy() {
138    SkASSERT(NULL != fPicture);
139    if (kNone_BBoxHierarchyType != fBBoxHierarchyType && NULL != fPicture) {
140        SkPicture* newPicture = this->createPicture();
141        SkCanvas* recorder = newPicture->beginRecording(fPicture->width(), fPicture->height(),
142                                                        this->recordFlags());
143        fPicture->draw(recorder);
144        newPicture->endRecording();
145        fPicture->unref();
146        fPicture = newPicture;
147    }
148}
149
150void PictureRenderer::resetState() {
151#if SK_SUPPORT_GPU
152    if (this->isUsingGpuDevice()) {
153        SkGLContext* glContext = fGrContextFactory.getGLContext(
154            GrContextFactory::kNative_GLContextType);
155
156        SkASSERT(glContext != NULL);
157        if (NULL == glContext) {
158            return;
159        }
160
161        fGrContext->flush();
162        SK_GL(*glContext, Finish());
163    }
164#endif
165}
166
167uint32_t PictureRenderer::recordFlags() {
168    return kNone_BBoxHierarchyType == fBBoxHierarchyType ? 0 :
169        SkPicture::kOptimizeForClippedPlayback_RecordingFlag;
170}
171
172/**
173 * Write the canvas to the specified path.
174 * @param canvas Must be non-null. Canvas to be written to a file.
175 * @param path Path for the file to be written. Should have no extension; write() will append
176 *             an appropriate one. Passed in by value so it can be modified.
177 * @return bool True if the Canvas is written to a file.
178 */
179static bool write(SkCanvas* canvas, SkString path) {
180    SkASSERT(canvas != NULL);
181    if (NULL == canvas) {
182        return false;
183    }
184
185    SkBitmap bitmap;
186    SkISize size = canvas->getDeviceSize();
187    sk_tools::setup_bitmap(&bitmap, size.width(), size.height());
188
189    canvas->readPixels(&bitmap, 0, 0);
190    sk_tools::force_all_opaque(bitmap);
191
192    // Since path is passed in by value, it is okay to modify it.
193    path.append(".png");
194    return SkImageEncoder::EncodeFile(path.c_str(), bitmap, SkImageEncoder::kPNG_Type, 100);
195}
196
197/**
198 * If path is non NULL, append number to it, and call write(SkCanvas*, SkString) to write the
199 * provided canvas to a file. Returns true if path is NULL or if write() succeeds.
200 */
201static bool writeAppendNumber(SkCanvas* canvas, const SkString* path, int number) {
202    if (NULL == path) {
203        return true;
204    }
205    SkString pathWithNumber(*path);
206    pathWithNumber.appendf("%i", number);
207    return write(canvas, pathWithNumber);
208}
209
210///////////////////////////////////////////////////////////////////////////////////////////////
211
212SkCanvas* RecordPictureRenderer::setupCanvas(int width, int height) {
213    // defer the canvas setup until the render step
214    return NULL;
215}
216
217static bool PNGEncodeBitmapToStream(SkWStream* wStream, const SkBitmap& bm) {
218    return SkImageEncoder::EncodeStream(wStream, bm, SkImageEncoder::kPNG_Type, 100);
219}
220
221bool RecordPictureRenderer::render(const SkString* path) {
222    SkAutoTUnref<SkPicture> replayer(this->createPicture());
223    SkCanvas* recorder = replayer->beginRecording(fPicture->width(), fPicture->height(),
224                                                  this->recordFlags());
225    fPicture->draw(recorder);
226    replayer->endRecording();
227    if (path != NULL) {
228        // Record the new picture as a new SKP with PNG encoded bitmaps.
229        SkString skpPath(*path);
230        // ".skp" was removed from 'path' before being passed in here.
231        skpPath.append(".skp");
232        SkFILEWStream stream(skpPath.c_str());
233        replayer->serialize(&stream, &PNGEncodeBitmapToStream);
234        return true;
235    }
236    return false;
237}
238
239SkString RecordPictureRenderer::getConfigNameInternal() {
240    return SkString("record");
241}
242
243///////////////////////////////////////////////////////////////////////////////////////////////
244
245bool PipePictureRenderer::render(const SkString* path) {
246    SkASSERT(fCanvas.get() != NULL);
247    SkASSERT(fPicture != NULL);
248    if (NULL == fCanvas.get() || NULL == fPicture) {
249        return false;
250    }
251
252    PipeController pipeController(fCanvas.get());
253    SkGPipeWriter writer;
254    SkCanvas* pipeCanvas = writer.startRecording(&pipeController);
255    pipeCanvas->drawPicture(*fPicture);
256    writer.endRecording();
257    fCanvas->flush();
258    if (NULL != path) {
259        return write(fCanvas, *path);
260    }
261    return true;
262}
263
264SkString PipePictureRenderer::getConfigNameInternal() {
265    return SkString("pipe");
266}
267
268///////////////////////////////////////////////////////////////////////////////////////////////
269
270void SimplePictureRenderer::init(SkPicture* picture) {
271    INHERITED::init(picture);
272    this->buildBBoxHierarchy();
273}
274
275bool SimplePictureRenderer::render(const SkString* path) {
276    SkASSERT(fCanvas.get() != NULL);
277    SkASSERT(fPicture != NULL);
278    if (NULL == fCanvas.get() || NULL == fPicture) {
279        return false;
280    }
281
282    fCanvas->drawPicture(*fPicture);
283    fCanvas->flush();
284    if (NULL != path) {
285        return write(fCanvas, *path);
286    }
287    return true;
288}
289
290SkString SimplePictureRenderer::getConfigNameInternal() {
291    return SkString("simple");
292}
293
294///////////////////////////////////////////////////////////////////////////////////////////////
295
296TiledPictureRenderer::TiledPictureRenderer()
297    : fTileWidth(kDefaultTileWidth)
298    , fTileHeight(kDefaultTileHeight)
299    , fTileWidthPercentage(0.0)
300    , fTileHeightPercentage(0.0)
301    , fTileMinPowerOf2Width(0) { }
302
303void TiledPictureRenderer::init(SkPicture* pict) {
304    SkASSERT(pict != NULL);
305    SkASSERT(0 == fTileRects.count());
306    if (NULL == pict || fTileRects.count() != 0) {
307        return;
308    }
309
310    // Do not call INHERITED::init(), which would create a (potentially large) canvas which is not
311    // used by bench_pictures.
312    fPicture = pict;
313    fPicture->ref();
314    this->buildBBoxHierarchy();
315
316    if (fTileWidthPercentage > 0) {
317        fTileWidth = sk_float_ceil2int(float(fTileWidthPercentage * fPicture->width() / 100));
318    }
319    if (fTileHeightPercentage > 0) {
320        fTileHeight = sk_float_ceil2int(float(fTileHeightPercentage * fPicture->height() / 100));
321    }
322
323    if (fTileMinPowerOf2Width > 0) {
324        this->setupPowerOf2Tiles();
325    } else {
326        this->setupTiles();
327    }
328}
329
330void TiledPictureRenderer::end() {
331    fTileRects.reset();
332    this->INHERITED::end();
333}
334
335void TiledPictureRenderer::setupTiles() {
336    for (int tile_y_start = 0; tile_y_start < fPicture->height(); tile_y_start += fTileHeight) {
337        for (int tile_x_start = 0; tile_x_start < fPicture->width(); tile_x_start += fTileWidth) {
338            *fTileRects.append() = SkRect::MakeXYWH(SkIntToScalar(tile_x_start),
339                                                    SkIntToScalar(tile_y_start),
340                                                    SkIntToScalar(fTileWidth),
341                                                    SkIntToScalar(fTileHeight));
342        }
343    }
344}
345
346// The goal of the powers of two tiles is to minimize the amount of wasted tile
347// space in the width-wise direction and then minimize the number of tiles. The
348// constraints are that every tile must have a pixel width that is a power of
349// two and also be of some minimal width (that is also a power of two).
350//
351// This is solved by first taking our picture size and rounding it up to the
352// multiple of the minimal width. The binary representation of this rounded
353// value gives us the tiles we need: a bit of value one means we need a tile of
354// that size.
355void TiledPictureRenderer::setupPowerOf2Tiles() {
356    int rounded_value = fPicture->width();
357    if (fPicture->width() % fTileMinPowerOf2Width != 0) {
358        rounded_value = fPicture->width() - (fPicture->width() % fTileMinPowerOf2Width)
359            + fTileMinPowerOf2Width;
360    }
361
362    int num_bits = SkScalarCeilToInt(SkScalarLog2(SkIntToScalar(fPicture->width())));
363    int largest_possible_tile_size = 1 << num_bits;
364
365    // The tile height is constant for a particular picture.
366    for (int tile_y_start = 0; tile_y_start < fPicture->height(); tile_y_start += fTileHeight) {
367        int tile_x_start = 0;
368        int current_width = largest_possible_tile_size;
369        // Set fTileWidth to be the width of the widest tile, so that each canvas is large enough
370        // to draw each tile.
371        fTileWidth = current_width;
372
373        while (current_width >= fTileMinPowerOf2Width) {
374            // It is very important this is a bitwise AND.
375            if (current_width & rounded_value) {
376                *fTileRects.append() = SkRect::MakeXYWH(SkIntToScalar(tile_x_start),
377                                                        SkIntToScalar(tile_y_start),
378                                                        SkIntToScalar(current_width),
379                                                        SkIntToScalar(fTileHeight));
380                tile_x_start += current_width;
381            }
382
383            current_width >>= 1;
384        }
385    }
386}
387
388/**
389 * Draw the specified playback to the canvas translated to rectangle provided, so that this mini
390 * canvas represents the rectangle's portion of the overall picture.
391 * Saves and restores so that the initial clip and matrix return to their state before this function
392 * is called.
393 */
394template<class T>
395static void DrawTileToCanvas(SkCanvas* canvas, const SkRect& tileRect, T* playback) {
396    int saveCount = canvas->save();
397    // Translate so that we draw the correct portion of the picture
398    canvas->translate(-tileRect.fLeft, -tileRect.fTop);
399    playback->draw(canvas);
400    canvas->restoreToCount(saveCount);
401    canvas->flush();
402}
403
404///////////////////////////////////////////////////////////////////////////////////////////////
405
406bool TiledPictureRenderer::render(const SkString* path) {
407    SkASSERT(fPicture != NULL);
408    if (NULL == fPicture) {
409        return false;
410    }
411
412    // Reuse one canvas for all tiles.
413    SkCanvas* canvas = this->setupCanvas(fTileWidth, fTileHeight);
414    SkAutoUnref aur(canvas);
415
416    bool success = true;
417    for (int i = 0; i < fTileRects.count(); ++i) {
418        DrawTileToCanvas(canvas, fTileRects[i], fPicture);
419        if (NULL != path) {
420            success &= writeAppendNumber(canvas, path, i);
421        }
422    }
423    return success;
424}
425
426SkCanvas* TiledPictureRenderer::setupCanvas(int width, int height) {
427    SkCanvas* canvas = this->INHERITED::setupCanvas(width, height);
428    SkASSERT(fPicture != NULL);
429    // Clip the tile to an area that is completely in what the SkPicture says is the
430    // drawn-to area. This is mostly important for tiles on the right and bottom edges
431    // as they may go over this area and the picture may have some commands that
432    // draw outside of this area and so should not actually be written.
433    SkRect clip = SkRect::MakeWH(SkIntToScalar(fPicture->width()),
434                                 SkIntToScalar(fPicture->height()));
435    canvas->clipRect(clip);
436    return canvas;
437}
438
439SkString TiledPictureRenderer::getConfigNameInternal() {
440    SkString name;
441    if (fTileMinPowerOf2Width > 0) {
442        name.append("pow2tile_");
443        name.appendf("%i", fTileMinPowerOf2Width);
444    } else {
445        name.append("tile_");
446        if (fTileWidthPercentage > 0) {
447            name.appendf("%.f%%", fTileWidthPercentage);
448        } else {
449            name.appendf("%i", fTileWidth);
450        }
451    }
452    name.append("x");
453    if (fTileHeightPercentage > 0) {
454        name.appendf("%.f%%", fTileHeightPercentage);
455    } else {
456        name.appendf("%i", fTileHeight);
457    }
458    return name;
459}
460
461///////////////////////////////////////////////////////////////////////////////////////////////
462
463// Holds all of the information needed to draw a set of tiles.
464class CloneData : public SkRunnable {
465
466public:
467    CloneData(SkPicture* clone, SkCanvas* canvas, SkTDArray<SkRect>& rects, int start, int end,
468              SkRunnable* done)
469        : fClone(clone)
470        , fCanvas(canvas)
471        , fPath(NULL)
472        , fRects(rects)
473        , fStart(start)
474        , fEnd(end)
475        , fSuccess(NULL)
476        , fDone(done) {
477        SkASSERT(fDone != NULL);
478    }
479
480    virtual void run() SK_OVERRIDE {
481        SkGraphics::SetTLSFontCacheLimit(1024 * 1024);
482        for (int i = fStart; i < fEnd; i++) {
483            DrawTileToCanvas(fCanvas, fRects[i], fClone);
484            if (fPath != NULL && !writeAppendNumber(fCanvas, fPath, i)
485                && fSuccess != NULL) {
486                *fSuccess = false;
487                // If one tile fails to write to a file, do not continue drawing the rest.
488                break;
489            }
490        }
491        fDone->run();
492    }
493
494    void setPathAndSuccess(const SkString* path, bool* success) {
495        fPath = path;
496        fSuccess = success;
497    }
498
499private:
500    // All pointers unowned.
501    SkPicture*         fClone;      // Picture to draw from. Each CloneData has a unique one which
502                                    // is threadsafe.
503    SkCanvas*          fCanvas;     // Canvas to draw to. Reused for each tile.
504    const SkString*    fPath;       // If non-null, path to write the result to as a PNG.
505    SkTDArray<SkRect>& fRects;      // All tiles of the picture.
506    const int          fStart;      // Range of tiles drawn by this thread.
507    const int          fEnd;
508    bool*              fSuccess;    // Only meaningful if path is non-null. Shared by all threads,
509                                    // and only set to false upon failure to write to a PNG.
510    SkRunnable*        fDone;
511};
512
513MultiCorePictureRenderer::MultiCorePictureRenderer(int threadCount)
514: fNumThreads(threadCount)
515, fThreadPool(threadCount)
516, fCountdown(threadCount) {
517    // Only need to create fNumThreads - 1 clones, since one thread will use the base
518    // picture.
519    fPictureClones = SkNEW_ARRAY(SkPicture, fNumThreads - 1);
520    fCloneData = SkNEW_ARRAY(CloneData*, fNumThreads);
521}
522
523void MultiCorePictureRenderer::init(SkPicture *pict) {
524    // Set fPicture and the tiles.
525    this->INHERITED::init(pict);
526    for (int i = 0; i < fNumThreads; ++i) {
527        *fCanvasPool.append() = this->setupCanvas(this->getTileWidth(), this->getTileHeight());
528    }
529    // Only need to create fNumThreads - 1 clones, since one thread will use the base picture.
530    fPicture->clone(fPictureClones, fNumThreads - 1);
531    // Populate each thread with the appropriate data.
532    // Group the tiles into nearly equal size chunks, rounding up so we're sure to cover them all.
533    const int chunkSize = (fTileRects.count() + fNumThreads - 1) / fNumThreads;
534
535    for (int i = 0; i < fNumThreads; i++) {
536        SkPicture* pic;
537        if (i == fNumThreads-1) {
538            // The last set will use the original SkPicture.
539            pic = fPicture;
540        } else {
541            pic = &fPictureClones[i];
542        }
543        const int start = i * chunkSize;
544        const int end = SkMin32(start + chunkSize, fTileRects.count());
545        fCloneData[i] = SkNEW_ARGS(CloneData,
546                                   (pic, fCanvasPool[i], fTileRects, start, end, &fCountdown));
547    }
548}
549
550bool MultiCorePictureRenderer::render(const SkString *path) {
551    bool success = true;
552    if (path != NULL) {
553        for (int i = 0; i < fNumThreads-1; i++) {
554            fCloneData[i]->setPathAndSuccess(path, &success);
555        }
556    }
557
558    fCountdown.reset(fNumThreads);
559    for (int i = 0; i < fNumThreads; i++) {
560        fThreadPool.add(fCloneData[i]);
561    }
562    fCountdown.wait();
563
564    return success;
565}
566
567void MultiCorePictureRenderer::end() {
568    for (int i = 0; i < fNumThreads - 1; i++) {
569        SkDELETE(fCloneData[i]);
570        fCloneData[i] = NULL;
571    }
572
573    fCanvasPool.unrefAll();
574
575    this->INHERITED::end();
576}
577
578MultiCorePictureRenderer::~MultiCorePictureRenderer() {
579    // Each individual CloneData was deleted in end.
580    SkDELETE_ARRAY(fCloneData);
581    SkDELETE_ARRAY(fPictureClones);
582}
583
584SkString MultiCorePictureRenderer::getConfigNameInternal() {
585    SkString name = this->INHERITED::getConfigNameInternal();
586    name.appendf("_multi_%i_threads", fNumThreads);
587    return name;
588}
589
590///////////////////////////////////////////////////////////////////////////////////////////////
591
592void PlaybackCreationRenderer::setup() {
593    fReplayer.reset(this->createPicture());
594    SkCanvas* recorder = fReplayer->beginRecording(fPicture->width(), fPicture->height(),
595                                                   this->recordFlags());
596    fPicture->draw(recorder);
597}
598
599bool PlaybackCreationRenderer::render(const SkString*) {
600    fReplayer->endRecording();
601    // Since this class does not actually render, return false.
602    return false;
603}
604
605SkString PlaybackCreationRenderer::getConfigNameInternal() {
606    return SkString("playback_creation");
607}
608
609///////////////////////////////////////////////////////////////////////////////////////////////
610// SkPicture variants for each BBoxHierarchy type
611
612class RTreePicture : public SkPicture {
613public:
614    virtual SkBBoxHierarchy* createBBoxHierarchy() const SK_OVERRIDE{
615        static const int kRTreeMinChildren = 6;
616        static const int kRTreeMaxChildren = 11;
617        SkScalar aspectRatio = SkScalarDiv(SkIntToScalar(fWidth),
618                                           SkIntToScalar(fHeight));
619        return SkRTree::Create(kRTreeMinChildren, kRTreeMaxChildren,
620                               aspectRatio);
621    }
622};
623
624class TileGridPicture : public SkPicture {
625public:
626    TileGridPicture(int tileWidth, int tileHeight, int xTileCount, int yTileCount) {
627        fTileWidth = tileWidth;
628        fTileHeight = tileHeight;
629        fXTileCount = xTileCount;
630        fYTileCount = yTileCount;
631    }
632
633    virtual SkBBoxHierarchy* createBBoxHierarchy() const SK_OVERRIDE{
634        return SkNEW_ARGS(SkTileGrid, (fTileWidth, fTileHeight, fXTileCount, fYTileCount));
635    }
636private:
637    int fTileWidth, fTileHeight, fXTileCount, fYTileCount;
638};
639
640SkPicture* PictureRenderer::createPicture() {
641    switch (fBBoxHierarchyType) {
642        case kNone_BBoxHierarchyType:
643            return SkNEW(SkPicture);
644        case kRTree_BBoxHierarchyType:
645            return SkNEW(RTreePicture);
646        case kTileGrid_BBoxHierarchyType:
647            {
648                int xTileCount = fPicture->width() / fGridWidth +
649                    ((fPicture->width() % fGridWidth) ? 1 : 0);
650                int yTileCount = fPicture->height() / fGridHeight +
651                    ((fPicture->height() % fGridHeight) ? 1 : 0);
652                return SkNEW_ARGS(TileGridPicture, (fGridWidth, fGridHeight, xTileCount,
653                                                    yTileCount));
654            }
655    }
656    SkASSERT(0); // invalid bbhType
657    return NULL;
658}
659
660} // namespace sk_tools
661