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