| /* |
| * Copyright 2012 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "include/core/SkBBHFactory.h" |
| #include "include/core/SkBitmap.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkClipOp.h" |
| #include "include/core/SkColor.h" |
| #include "include/core/SkData.h" |
| #include "include/core/SkFontStyle.h" |
| #include "include/core/SkImageInfo.h" |
| #include "include/core/SkMatrix.h" |
| #include "include/core/SkPaint.h" |
| #include "include/core/SkPath.h" |
| #include "include/core/SkPictureRecorder.h" |
| #include "include/core/SkPixelRef.h" |
| #include "include/core/SkRect.h" |
| #include "include/core/SkRefCnt.h" |
| #include "include/core/SkScalar.h" |
| #include "include/core/SkShader.h" |
| #include "include/core/SkStream.h" |
| #include "include/core/SkTypeface.h" |
| #include "include/core/SkTypes.h" |
| #include "include/utils/SkRandom.h" |
| #include "src/core/SkBigPicture.h" |
| #include "src/core/SkClipOpPriv.h" |
| #include "src/core/SkMiniRecorder.h" |
| #include "src/core/SkPicturePriv.h" |
| #include "src/core/SkRectPriv.h" |
| #include "tests/Test.h" |
| |
| #include <memory> |
| |
| class SkRRect; |
| class SkRegion; |
| template <typename T> class SkTDArray; |
| |
| |
| static void make_bm(SkBitmap* bm, int w, int h, SkColor color, bool immutable) { |
| bm->allocN32Pixels(w, h); |
| bm->eraseColor(color); |
| if (immutable) { |
| bm->setImmutable(); |
| } |
| } |
| |
| #ifdef SK_DEBUG |
| // Ensure that deleting an empty SkPicture does not assert. Asserts only fire |
| // in debug mode, so only run in debug mode. |
| static void test_deleting_empty_picture() { |
| SkPictureRecorder recorder; |
| // Creates an SkPictureRecord |
| recorder.beginRecording(0, 0); |
| // Turns that into an SkPicture |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| // Ceates a new SkPictureRecord |
| recorder.beginRecording(0, 0); |
| } |
| |
| // Ensure that serializing an empty picture does not assert. Likewise only runs in debug mode. |
| static void test_serializing_empty_picture() { |
| SkPictureRecorder recorder; |
| recorder.beginRecording(0, 0); |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| SkDynamicMemoryWStream stream; |
| picture->serialize(&stream); |
| } |
| #endif |
| |
| static void rand_op(SkCanvas* canvas, SkRandom& rand) { |
| SkPaint paint; |
| SkRect rect = SkRect::MakeWH(50, 50); |
| |
| SkScalar unit = rand.nextUScalar1(); |
| if (unit <= 0.3) { |
| // SkDebugf("save\n"); |
| canvas->save(); |
| } else if (unit <= 0.6) { |
| // SkDebugf("restore\n"); |
| canvas->restore(); |
| } else if (unit <= 0.9) { |
| // SkDebugf("clip\n"); |
| canvas->clipRect(rect); |
| } else { |
| // SkDebugf("draw\n"); |
| canvas->drawPaint(paint); |
| } |
| } |
| |
| static void set_canvas_to_save_count_4(SkCanvas* canvas) { |
| canvas->restoreToCount(1); |
| canvas->save(); |
| canvas->save(); |
| canvas->save(); |
| } |
| |
| /** |
| * A canvas that records the number of saves, saveLayers and restores. |
| */ |
| class SaveCountingCanvas : public SkCanvas { |
| public: |
| SaveCountingCanvas(int width, int height) |
| : INHERITED(width, height) |
| , fSaveCount(0) |
| , fSaveLayerCount(0) |
| , fSaveBehindCount(0) |
| , fRestoreCount(0){ |
| } |
| |
| SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec& rec) override { |
| ++fSaveLayerCount; |
| return this->INHERITED::getSaveLayerStrategy(rec); |
| } |
| |
| bool onDoSaveBehind(const SkRect* subset) override { |
| ++fSaveBehindCount; |
| return this->INHERITED::onDoSaveBehind(subset); |
| } |
| |
| void willSave() override { |
| ++fSaveCount; |
| this->INHERITED::willSave(); |
| } |
| |
| void willRestore() override { |
| ++fRestoreCount; |
| this->INHERITED::willRestore(); |
| } |
| |
| unsigned int getSaveCount() const { return fSaveCount; } |
| unsigned int getSaveLayerCount() const { return fSaveLayerCount; } |
| unsigned int getSaveBehindCount() const { return fSaveBehindCount; } |
| unsigned int getRestoreCount() const { return fRestoreCount; } |
| |
| private: |
| unsigned int fSaveCount; |
| unsigned int fSaveLayerCount; |
| unsigned int fSaveBehindCount; |
| unsigned int fRestoreCount; |
| |
| using INHERITED = SkCanvas; |
| }; |
| |
| void check_save_state(skiatest::Reporter* reporter, SkPicture* picture, |
| unsigned int numSaves, unsigned int numSaveLayers, |
| unsigned int numRestores) { |
| SaveCountingCanvas canvas(SkScalarCeilToInt(picture->cullRect().width()), |
| SkScalarCeilToInt(picture->cullRect().height())); |
| |
| picture->playback(&canvas); |
| |
| // Optimizations may have removed these, |
| // so expect to have seen no more than num{Saves,SaveLayers,Restores}. |
| REPORTER_ASSERT(reporter, numSaves >= canvas.getSaveCount()); |
| REPORTER_ASSERT(reporter, numSaveLayers >= canvas.getSaveLayerCount()); |
| REPORTER_ASSERT(reporter, numRestores >= canvas.getRestoreCount()); |
| } |
| |
| // This class exists so SkPicture can friend it and give it access to |
| // the 'partialReplay' method. |
| class SkPictureRecorderReplayTester { |
| public: |
| static sk_sp<SkPicture> Copy(SkPictureRecorder* recorder) { |
| SkPictureRecorder recorder2; |
| |
| SkCanvas* canvas = recorder2.beginRecording(10, 10); |
| |
| recorder->partialReplay(canvas); |
| |
| return recorder2.finishRecordingAsPicture(); |
| } |
| }; |
| |
| static void create_imbalance(SkCanvas* canvas) { |
| SkRect clipRect = SkRect::MakeWH(2, 2); |
| SkRect drawRect = SkRect::MakeWH(10, 10); |
| canvas->save(); |
| canvas->clipRect(clipRect, kReplace_SkClipOp); |
| canvas->translate(1.0f, 1.0f); |
| SkPaint p; |
| p.setColor(SK_ColorGREEN); |
| canvas->drawRect(drawRect, p); |
| // no restore |
| } |
| |
| // This tests that replaying a potentially unbalanced picture into a canvas |
| // doesn't affect the canvas' save count or matrix/clip state. |
| static void check_balance(skiatest::Reporter* reporter, SkPicture* picture) { |
| SkBitmap bm; |
| bm.allocN32Pixels(4, 3); |
| SkCanvas canvas(bm); |
| |
| int beforeSaveCount = canvas.getSaveCount(); |
| |
| SkMatrix beforeMatrix = canvas.getTotalMatrix(); |
| |
| SkRect beforeClip = canvas.getLocalClipBounds(); |
| |
| canvas.drawPicture(picture); |
| |
| REPORTER_ASSERT(reporter, beforeSaveCount == canvas.getSaveCount()); |
| REPORTER_ASSERT(reporter, beforeMatrix == canvas.getTotalMatrix()); |
| |
| SkRect afterClip = canvas.getLocalClipBounds(); |
| |
| REPORTER_ASSERT(reporter, afterClip == beforeClip); |
| } |
| |
| // Test out SkPictureRecorder::partialReplay |
| DEF_TEST(PictureRecorder_replay, reporter) { |
| // check save/saveLayer state |
| { |
| SkPictureRecorder recorder; |
| |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| |
| canvas->saveLayer(nullptr, nullptr); |
| |
| sk_sp<SkPicture> copy(SkPictureRecorderReplayTester::Copy(&recorder)); |
| |
| // The extra save and restore comes from the Copy process. |
| check_save_state(reporter, copy.get(), 2, 1, 3); |
| |
| canvas->saveLayer(nullptr, nullptr); |
| |
| sk_sp<SkPicture> final(recorder.finishRecordingAsPicture()); |
| |
| check_save_state(reporter, final.get(), 1, 2, 3); |
| |
| // The copy shouldn't pick up any operations added after it was made |
| check_save_state(reporter, copy.get(), 2, 1, 3); |
| } |
| |
| // Recreate the Android partialReplay test case |
| { |
| SkPictureRecorder recorder; |
| |
| SkCanvas* canvas = recorder.beginRecording(4, 3); |
| create_imbalance(canvas); |
| |
| int expectedSaveCount = canvas->getSaveCount(); |
| |
| sk_sp<SkPicture> copy(SkPictureRecorderReplayTester::Copy(&recorder)); |
| check_balance(reporter, copy.get()); |
| |
| REPORTER_ASSERT(reporter, expectedSaveCount = canvas->getSaveCount()); |
| |
| // End the recording of source to test the picture finalization |
| // process isn't complicated by the partialReplay step |
| sk_sp<SkPicture> final(recorder.finishRecordingAsPicture()); |
| } |
| } |
| |
| static void test_unbalanced_save_restores(skiatest::Reporter* reporter) { |
| SkCanvas testCanvas(100, 100); |
| set_canvas_to_save_count_4(&testCanvas); |
| |
| REPORTER_ASSERT(reporter, 4 == testCanvas.getSaveCount()); |
| |
| SkPaint paint; |
| SkRect rect = SkRect::MakeLTRB(-10000000, -10000000, 10000000, 10000000); |
| |
| SkPictureRecorder recorder; |
| |
| { |
| // Create picture with 2 unbalanced saves |
| SkCanvas* canvas = recorder.beginRecording(100, 100); |
| canvas->save(); |
| canvas->translate(10, 10); |
| canvas->drawRect(rect, paint); |
| canvas->save(); |
| canvas->translate(10, 10); |
| canvas->drawRect(rect, paint); |
| sk_sp<SkPicture> extraSavePicture(recorder.finishRecordingAsPicture()); |
| |
| testCanvas.drawPicture(extraSavePicture); |
| REPORTER_ASSERT(reporter, 4 == testCanvas.getSaveCount()); |
| } |
| |
| set_canvas_to_save_count_4(&testCanvas); |
| |
| { |
| // Create picture with 2 unbalanced restores |
| SkCanvas* canvas = recorder.beginRecording(100, 100); |
| canvas->save(); |
| canvas->translate(10, 10); |
| canvas->drawRect(rect, paint); |
| canvas->save(); |
| canvas->translate(10, 10); |
| canvas->drawRect(rect, paint); |
| canvas->restore(); |
| canvas->restore(); |
| canvas->restore(); |
| canvas->restore(); |
| sk_sp<SkPicture> extraRestorePicture(recorder.finishRecordingAsPicture()); |
| |
| testCanvas.drawPicture(extraRestorePicture); |
| REPORTER_ASSERT(reporter, 4 == testCanvas.getSaveCount()); |
| } |
| |
| set_canvas_to_save_count_4(&testCanvas); |
| |
| { |
| SkCanvas* canvas = recorder.beginRecording(100, 100); |
| canvas->translate(10, 10); |
| canvas->drawRect(rect, paint); |
| sk_sp<SkPicture> noSavePicture(recorder.finishRecordingAsPicture()); |
| |
| testCanvas.drawPicture(noSavePicture); |
| REPORTER_ASSERT(reporter, 4 == testCanvas.getSaveCount()); |
| REPORTER_ASSERT(reporter, testCanvas.getTotalMatrix().isIdentity()); |
| } |
| } |
| |
| static void test_peephole() { |
| SkRandom rand; |
| |
| SkPictureRecorder recorder; |
| |
| for (int j = 0; j < 100; j++) { |
| SkRandom rand2(rand); // remember the seed |
| |
| SkCanvas* canvas = recorder.beginRecording(100, 100); |
| |
| for (int i = 0; i < 1000; ++i) { |
| rand_op(canvas, rand); |
| } |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| |
| rand = rand2; |
| } |
| |
| { |
| SkCanvas* canvas = recorder.beginRecording(100, 100); |
| SkRect rect = SkRect::MakeWH(50, 50); |
| |
| for (int i = 0; i < 100; ++i) { |
| canvas->save(); |
| } |
| while (canvas->getSaveCount() > 1) { |
| canvas->clipRect(rect); |
| canvas->restore(); |
| } |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| } |
| } |
| |
| static void test_bad_bitmap(skiatest::Reporter* reporter) { |
| // missing pixels should return null for image |
| SkBitmap bm; |
| bm.setInfo(SkImageInfo::MakeN32Premul(100, 100)); |
| auto img = bm.asImage(); |
| REPORTER_ASSERT(reporter, !img); |
| |
| // make sure we don't crash on a null image |
| SkPictureRecorder recorder; |
| SkCanvas* recordingCanvas = recorder.beginRecording(100, 100); |
| recordingCanvas->drawImage(nullptr, 0, 0); |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| |
| SkCanvas canvas; |
| canvas.drawPicture(picture); |
| } |
| |
| static void test_clip_bound_opt(skiatest::Reporter* reporter) { |
| // Test for crbug.com/229011 |
| SkRect rect1 = SkRect::MakeXYWH(SkIntToScalar(4), SkIntToScalar(4), |
| SkIntToScalar(2), SkIntToScalar(2)); |
| SkRect rect2 = SkRect::MakeXYWH(SkIntToScalar(7), SkIntToScalar(7), |
| SkIntToScalar(1), SkIntToScalar(1)); |
| SkRect rect3 = SkRect::MakeXYWH(SkIntToScalar(6), SkIntToScalar(6), |
| SkIntToScalar(1), SkIntToScalar(1)); |
| |
| SkPath invPath; |
| invPath.addOval(rect1); |
| invPath.setFillType(SkPathFillType::kInverseEvenOdd); |
| SkPath path; |
| path.addOval(rect2); |
| SkPath path2; |
| path2.addOval(rect3); |
| SkIRect clipBounds; |
| SkPictureRecorder recorder; |
| |
| // Testing conservative-raster-clip that is enabled by PictureRecord |
| { |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| canvas->clipPath(invPath); |
| clipBounds = canvas->getDeviceClipBounds(); |
| REPORTER_ASSERT(reporter, 0 == clipBounds.fLeft); |
| REPORTER_ASSERT(reporter, 0 == clipBounds.fTop); |
| REPORTER_ASSERT(reporter, 10 == clipBounds.fBottom); |
| REPORTER_ASSERT(reporter, 10 == clipBounds.fRight); |
| } |
| { |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| canvas->clipPath(path); |
| canvas->clipPath(invPath); |
| clipBounds = canvas->getDeviceClipBounds(); |
| REPORTER_ASSERT(reporter, 7 == clipBounds.fLeft); |
| REPORTER_ASSERT(reporter, 7 == clipBounds.fTop); |
| REPORTER_ASSERT(reporter, 8 == clipBounds.fBottom); |
| REPORTER_ASSERT(reporter, 8 == clipBounds.fRight); |
| } |
| { |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| canvas->clipPath(path); |
| canvas->clipPath(invPath, kUnion_SkClipOp); |
| clipBounds = canvas->getDeviceClipBounds(); |
| REPORTER_ASSERT(reporter, 0 == clipBounds.fLeft); |
| REPORTER_ASSERT(reporter, 0 == clipBounds.fTop); |
| REPORTER_ASSERT(reporter, 10 == clipBounds.fBottom); |
| REPORTER_ASSERT(reporter, 10 == clipBounds.fRight); |
| } |
| { |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| canvas->clipPath(path, kDifference_SkClipOp); |
| clipBounds = canvas->getDeviceClipBounds(); |
| REPORTER_ASSERT(reporter, 0 == clipBounds.fLeft); |
| REPORTER_ASSERT(reporter, 0 == clipBounds.fTop); |
| REPORTER_ASSERT(reporter, 10 == clipBounds.fBottom); |
| REPORTER_ASSERT(reporter, 10 == clipBounds.fRight); |
| } |
| { |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| canvas->clipPath(path, kReverseDifference_SkClipOp); |
| clipBounds = canvas->getDeviceClipBounds(); |
| // True clip is actually empty in this case, but the best |
| // determination we can make using only bounds as input is that the |
| // clip is included in the bounds of 'path'. |
| REPORTER_ASSERT(reporter, 7 == clipBounds.fLeft); |
| REPORTER_ASSERT(reporter, 7 == clipBounds.fTop); |
| REPORTER_ASSERT(reporter, 8 == clipBounds.fBottom); |
| REPORTER_ASSERT(reporter, 8 == clipBounds.fRight); |
| } |
| { |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| canvas->clipPath(path, kIntersect_SkClipOp); |
| canvas->clipPath(path2, kXOR_SkClipOp); |
| clipBounds = canvas->getDeviceClipBounds(); |
| REPORTER_ASSERT(reporter, 6 == clipBounds.fLeft); |
| REPORTER_ASSERT(reporter, 6 == clipBounds.fTop); |
| REPORTER_ASSERT(reporter, 8 == clipBounds.fBottom); |
| REPORTER_ASSERT(reporter, 8 == clipBounds.fRight); |
| } |
| } |
| |
| static void test_cull_rect_reset(skiatest::Reporter* reporter) { |
| SkPictureRecorder recorder; |
| SkRect bounds = SkRect::MakeWH(10, 10); |
| SkRTreeFactory factory; |
| SkCanvas* canvas = recorder.beginRecording(bounds, &factory); |
| bounds = SkRect::MakeWH(100, 100); |
| SkPaint paint; |
| canvas->drawRect(bounds, paint); |
| canvas->drawRect(bounds, paint); |
| sk_sp<SkPicture> p(recorder.finishRecordingAsPictureWithCull(bounds)); |
| const SkBigPicture* picture = SkPicturePriv::AsSkBigPicture(p); |
| REPORTER_ASSERT(reporter, picture); |
| |
| SkRect finalCullRect = picture->cullRect(); |
| REPORTER_ASSERT(reporter, 0 == finalCullRect.fLeft); |
| REPORTER_ASSERT(reporter, 0 == finalCullRect.fTop); |
| REPORTER_ASSERT(reporter, 100 == finalCullRect.fBottom); |
| REPORTER_ASSERT(reporter, 100 == finalCullRect.fRight); |
| } |
| |
| |
| /** |
| * A canvas that records the number of clip commands. |
| */ |
| class ClipCountingCanvas : public SkCanvas { |
| public: |
| ClipCountingCanvas(int width, int height) |
| : INHERITED(width, height) |
| , fClipCount(0){ |
| } |
| |
| void onClipRect(const SkRect& r, SkClipOp op, ClipEdgeStyle edgeStyle) override { |
| fClipCount += 1; |
| this->INHERITED::onClipRect(r, op, edgeStyle); |
| } |
| |
| void onClipRRect(const SkRRect& rrect, SkClipOp op, ClipEdgeStyle edgeStyle)override { |
| fClipCount += 1; |
| this->INHERITED::onClipRRect(rrect, op, edgeStyle); |
| } |
| |
| void onClipPath(const SkPath& path, SkClipOp op, ClipEdgeStyle edgeStyle) override { |
| fClipCount += 1; |
| this->INHERITED::onClipPath(path, op, edgeStyle); |
| } |
| |
| void onClipRegion(const SkRegion& deviceRgn, SkClipOp op) override { |
| fClipCount += 1; |
| this->INHERITED::onClipRegion(deviceRgn, op); |
| } |
| |
| unsigned getClipCount() const { return fClipCount; } |
| |
| private: |
| unsigned fClipCount; |
| |
| using INHERITED = SkCanvas; |
| }; |
| |
| static void test_clip_expansion(skiatest::Reporter* reporter) { |
| SkPictureRecorder recorder; |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| |
| canvas->clipRect(SkRect::MakeEmpty(), kReplace_SkClipOp); |
| // The following expanding clip should not be skipped. |
| canvas->clipRect(SkRect::MakeXYWH(4, 4, 3, 3), kUnion_SkClipOp); |
| // Draw something so the optimizer doesn't just fold the world. |
| SkPaint p; |
| p.setColor(SK_ColorBLUE); |
| canvas->drawPaint(p); |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| |
| ClipCountingCanvas testCanvas(10, 10); |
| picture->playback(&testCanvas); |
| |
| // Both clips should be present on playback. |
| REPORTER_ASSERT(reporter, testCanvas.getClipCount() == 2); |
| } |
| |
| static void test_gen_id(skiatest::Reporter* reporter) { |
| |
| SkPictureRecorder recorder; |
| recorder.beginRecording(0, 0); |
| sk_sp<SkPicture> empty(recorder.finishRecordingAsPicture()); |
| |
| // Empty pictures should still have a valid ID |
| REPORTER_ASSERT(reporter, empty->uniqueID() != SK_InvalidGenID); |
| |
| SkCanvas* canvas = recorder.beginRecording(1, 1); |
| canvas->drawColor(SK_ColorWHITE); |
| sk_sp<SkPicture> hasData(recorder.finishRecordingAsPicture()); |
| // picture should have a non-zero id after recording |
| REPORTER_ASSERT(reporter, hasData->uniqueID() != SK_InvalidGenID); |
| |
| // both pictures should have different ids |
| REPORTER_ASSERT(reporter, hasData->uniqueID() != empty->uniqueID()); |
| } |
| |
| static void test_typeface(skiatest::Reporter* reporter) { |
| SkPictureRecorder recorder; |
| SkCanvas* canvas = recorder.beginRecording(10, 10); |
| SkFont font(SkTypeface::MakeFromName("Arial", SkFontStyle::Italic())); |
| canvas->drawString("Q", 0, 10, font, SkPaint()); |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| SkDynamicMemoryWStream stream; |
| picture->serialize(&stream); |
| } |
| |
| DEF_TEST(Picture, reporter) { |
| test_typeface(reporter); |
| #ifdef SK_DEBUG |
| test_deleting_empty_picture(); |
| test_serializing_empty_picture(); |
| #endif |
| test_bad_bitmap(reporter); |
| test_unbalanced_save_restores(reporter); |
| test_peephole(); |
| test_clip_bound_opt(reporter); |
| test_clip_expansion(reporter); |
| test_gen_id(reporter); |
| test_cull_rect_reset(reporter); |
| } |
| |
| static void draw_bitmaps(const SkBitmap bitmap, SkCanvas* canvas) { |
| const SkRect rect = { 5.0f, 5.0f, 8.0f, 8.0f }; |
| auto img = bitmap.asImage(); |
| |
| // Don't care what these record, as long as they're legal. |
| canvas->drawImage(img, 0.0f, 0.0f); |
| canvas->drawImageRect(img, rect, rect, SkSamplingOptions(), nullptr, |
| SkCanvas::kStrict_SrcRectConstraint); |
| canvas->drawImage(img, 1, 1); // drawSprite |
| } |
| |
| static void test_draw_bitmaps(SkCanvas* canvas) { |
| SkBitmap empty; |
| draw_bitmaps(empty, canvas); |
| empty.setInfo(SkImageInfo::MakeN32Premul(10, 10)); |
| draw_bitmaps(empty, canvas); |
| } |
| |
| DEF_TEST(Picture_EmptyBitmap, r) { |
| SkPictureRecorder recorder; |
| test_draw_bitmaps(recorder.beginRecording(10, 10)); |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| } |
| |
| DEF_TEST(Canvas_EmptyBitmap, r) { |
| SkBitmap dst; |
| dst.allocN32Pixels(10, 10); |
| SkCanvas canvas(dst); |
| |
| test_draw_bitmaps(&canvas); |
| } |
| |
| DEF_TEST(DontOptimizeSaveLayerDrawDrawRestore, reporter) { |
| // This test is from crbug.com/344987. |
| // The commands are: |
| // saveLayer with paint that modifies alpha |
| // drawBitmapRect |
| // drawBitmapRect |
| // restore |
| // The bug was that this structure was modified so that: |
| // - The saveLayer and restore were eliminated |
| // - The alpha was only applied to the first drawBitmapRectToRect |
| |
| // This test draws blue and red squares inside a 50% transparent |
| // layer. Both colours should show up muted. |
| // When the bug is present, the red square (the second bitmap) |
| // shows upwith full opacity. |
| |
| SkBitmap blueBM; |
| make_bm(&blueBM, 100, 100, SkColorSetARGB(255, 0, 0, 255), true); |
| SkBitmap redBM; |
| make_bm(&redBM, 100, 100, SkColorSetARGB(255, 255, 0, 0), true); |
| SkPaint semiTransparent; |
| semiTransparent.setAlpha(0x80); |
| |
| SkPictureRecorder recorder; |
| SkCanvas* canvas = recorder.beginRecording(100, 100); |
| canvas->drawColor(0); |
| |
| canvas->saveLayer(nullptr, &semiTransparent); |
| canvas->drawImage(blueBM.asImage(), 25, 25); |
| canvas->drawImage(redBM.asImage(), 50, 50); |
| canvas->restore(); |
| |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| |
| // Now replay the picture back on another canvas |
| // and check a couple of its pixels. |
| SkBitmap replayBM; |
| make_bm(&replayBM, 100, 100, SK_ColorBLACK, false); |
| SkCanvas replayCanvas(replayBM); |
| picture->playback(&replayCanvas); |
| |
| // With the bug present, at (55, 55) we would get a fully opaque red |
| // intead of a dark red. |
| REPORTER_ASSERT(reporter, replayBM.getColor(30, 30) == 0xff000080); |
| REPORTER_ASSERT(reporter, replayBM.getColor(55, 55) == 0xff800000); |
| } |
| |
| struct CountingBBH : public SkBBoxHierarchy { |
| mutable int searchCalls; |
| |
| CountingBBH() : searchCalls(0) {} |
| |
| void search(const SkRect& query, std::vector<int>* results) const override { |
| this->searchCalls++; |
| } |
| |
| void insert(const SkRect[], int) override {} |
| size_t bytesUsed() const override { return 0; } |
| }; |
| |
| class SpoonFedBBHFactory : public SkBBHFactory { |
| public: |
| explicit SpoonFedBBHFactory(sk_sp<SkBBoxHierarchy> bbh) : fBBH(bbh) {} |
| sk_sp<SkBBoxHierarchy> operator()() const override { |
| return fBBH; |
| } |
| private: |
| sk_sp<SkBBoxHierarchy> fBBH; |
| }; |
| |
| // When the canvas clip covers the full picture, we don't need to call the BBH. |
| DEF_TEST(Picture_SkipBBH, r) { |
| SkRect bound = SkRect::MakeWH(320, 240); |
| |
| auto bbh = sk_make_sp<CountingBBH>(); |
| SpoonFedBBHFactory factory(bbh); |
| |
| SkPictureRecorder recorder; |
| SkCanvas* c = recorder.beginRecording(bound, &factory); |
| // Record a few ops so we don't hit a small- or empty- picture optimization. |
| c->drawRect(bound, SkPaint()); |
| c->drawRect(bound, SkPaint()); |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| |
| SkCanvas big(640, 480), small(300, 200); |
| |
| picture->playback(&big); |
| REPORTER_ASSERT(r, bbh->searchCalls == 0); |
| |
| picture->playback(&small); |
| REPORTER_ASSERT(r, bbh->searchCalls == 1); |
| } |
| |
| DEF_TEST(Picture_BitmapLeak, r) { |
| SkBitmap mut, immut; |
| mut.allocN32Pixels(300, 200); |
| immut.allocN32Pixels(300, 200); |
| immut.setImmutable(); |
| SkASSERT(!mut.isImmutable()); |
| SkASSERT(immut.isImmutable()); |
| |
| // No one can hold a ref on our pixels yet. |
| REPORTER_ASSERT(r, mut.pixelRef()->unique()); |
| REPORTER_ASSERT(r, immut.pixelRef()->unique()); |
| |
| sk_sp<SkPicture> pic; |
| { |
| // we want the recorder to go out of scope before our subsequent checks, so we |
| // place it inside local braces. |
| SkPictureRecorder rec; |
| SkCanvas* canvas = rec.beginRecording(1920, 1200); |
| canvas->drawImage(mut.asImage(), 0, 0); |
| canvas->drawImage(immut.asImage(), 800, 600); |
| pic = rec.finishRecordingAsPicture(); |
| } |
| |
| // The picture shares the immutable pixels but copies the mutable ones. |
| REPORTER_ASSERT(r, mut.pixelRef()->unique()); |
| REPORTER_ASSERT(r, !immut.pixelRef()->unique()); |
| |
| // When the picture goes away, it's just our bitmaps holding the refs. |
| pic = nullptr; |
| REPORTER_ASSERT(r, mut.pixelRef()->unique()); |
| REPORTER_ASSERT(r, immut.pixelRef()->unique()); |
| } |
| |
| // getRecordingCanvas() should return a SkCanvas when recording, null when not recording. |
| DEF_TEST(Picture_getRecordingCanvas, r) { |
| SkPictureRecorder rec; |
| REPORTER_ASSERT(r, !rec.getRecordingCanvas()); |
| for (int i = 0; i < 3; i++) { |
| rec.beginRecording(100, 100); |
| REPORTER_ASSERT(r, rec.getRecordingCanvas()); |
| rec.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, !rec.getRecordingCanvas()); |
| } |
| } |
| |
| DEF_TEST(MiniRecorderLeftHanging, r) { |
| // Any shader or other ref-counted effect will do just fine here. |
| SkPaint paint; |
| paint.setShader(SkShaders::Color(SK_ColorRED)); |
| |
| SkMiniRecorder rec; |
| REPORTER_ASSERT(r, rec.drawRect(SkRect::MakeWH(20,30), paint)); |
| // Don't call rec.detachPicture(). Test succeeds by not asserting or leaking the shader. |
| } |
| |
| DEF_TEST(Picture_preserveCullRect, r) { |
| SkPictureRecorder recorder; |
| |
| SkCanvas* c = recorder.beginRecording(SkRect::MakeLTRB(1, 2, 3, 4)); |
| c->clear(SK_ColorCYAN); |
| |
| sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture()); |
| SkDynamicMemoryWStream wstream; |
| picture->serialize(&wstream); |
| |
| std::unique_ptr<SkStream> rstream(wstream.detachAsStream()); |
| sk_sp<SkPicture> deserializedPicture(SkPicture::MakeFromStream(rstream.get())); |
| |
| REPORTER_ASSERT(r, deserializedPicture != nullptr); |
| REPORTER_ASSERT(r, deserializedPicture->cullRect().left() == 1); |
| REPORTER_ASSERT(r, deserializedPicture->cullRect().top() == 2); |
| REPORTER_ASSERT(r, deserializedPicture->cullRect().right() == 3); |
| REPORTER_ASSERT(r, deserializedPicture->cullRect().bottom() == 4); |
| } |
| |
| |
| // If we record bounded ops into a picture with a big cull and calculate the |
| // bounds of those ops, we should trim down the picture cull to the ops' bounds. |
| // If we're not using an SkBBH, we shouldn't change it. |
| DEF_TEST(Picture_UpdatedCull_1, r) { |
| // Testing 1 draw exercises SkMiniPicture. |
| SkRTreeFactory factory; |
| SkPictureRecorder recorder; |
| |
| auto canvas = recorder.beginRecording(SkRectPriv::MakeLargest(), &factory); |
| canvas->drawRect(SkRect::MakeWH(20,20), SkPaint{}); |
| auto pic = recorder.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, pic->cullRect() == SkRect::MakeWH(20,20)); |
| |
| canvas = recorder.beginRecording(SkRectPriv::MakeLargest()); |
| canvas->drawRect(SkRect::MakeWH(20,20), SkPaint{}); |
| pic = recorder.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, pic->cullRect() == SkRectPriv::MakeLargest()); |
| } |
| DEF_TEST(Picture_UpdatedCull_2, r) { |
| // Testing >1 draw exercises SkBigPicture. |
| SkRTreeFactory factory; |
| SkPictureRecorder recorder; |
| |
| auto canvas = recorder.beginRecording(SkRectPriv::MakeLargest(), &factory); |
| canvas->drawRect(SkRect::MakeWH(20,20), SkPaint{}); |
| canvas->drawRect(SkRect::MakeWH(10,40), SkPaint{}); |
| auto pic = recorder.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, pic->cullRect() == SkRect::MakeWH(20,40)); |
| |
| canvas = recorder.beginRecording(SkRectPriv::MakeLargest()); |
| canvas->drawRect(SkRect::MakeWH(20,20), SkPaint{}); |
| canvas->drawRect(SkRect::MakeWH(10,40), SkPaint{}); |
| pic = recorder.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, pic->cullRect() == SkRectPriv::MakeLargest()); |
| } |
| |
| DEF_TEST(Picture_RecordsFlush, r) { |
| SkPictureRecorder recorder; |
| |
| auto canvas = recorder.beginRecording(SkRect::MakeWH(100,100)); |
| for (int i = 0; i < 10; i++) { |
| canvas->clear(0); |
| for (int j = 0; j < 10; j++) { |
| canvas->drawRect(SkRect::MakeXYWH(i*10,j*10,10,10), SkPaint()); |
| } |
| canvas->flush(); |
| } |
| |
| // Did we record the flushes? |
| auto pic = recorder.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, pic->approximateOpCount() == 120); // 10 clears, 100 draws, 10 flushes |
| |
| // Do we serialize and deserialize flushes? |
| auto skp = pic->serialize(); |
| auto back = SkPicture::MakeFromData(skp->data(), skp->size()); |
| REPORTER_ASSERT(r, back->approximateOpCount() == pic->approximateOpCount()); |
| } |
| |
| DEF_TEST(Placeholder, r) { |
| SkRect cull = { 0,0, 10,20 }; |
| |
| // Each placeholder is unique. |
| sk_sp<SkPicture> p1 = SkPicture::MakePlaceholder(cull), |
| p2 = SkPicture::MakePlaceholder(cull); |
| REPORTER_ASSERT(r, p1->cullRect() == p2->cullRect()); |
| REPORTER_ASSERT(r, p1->cullRect() == cull); |
| REPORTER_ASSERT(r, p1->uniqueID() != p2->uniqueID()); |
| |
| // Placeholders are never unrolled by SkCanvas (while other small pictures may be). |
| SkPictureRecorder recorder; |
| SkCanvas* canvas = recorder.beginRecording(cull); |
| canvas->drawPicture(p1); |
| canvas->drawPicture(p2); |
| sk_sp<SkPicture> pic = recorder.finishRecordingAsPicture(); |
| REPORTER_ASSERT(r, pic->approximateOpCount() == 2); |
| |
| // Any upper limit when recursing into nested placeholders is fine as long |
| // as it doesn't overflow an int. |
| REPORTER_ASSERT(r, pic->approximateOpCount(/*nested?*/true) >= 2); |
| REPORTER_ASSERT(r, pic->approximateOpCount(/*nested?*/true) <= 10); |
| } |
| |
| DEF_TEST(Picture_empty_serial, reporter) { |
| SkPictureRecorder rec; |
| (void)rec.beginRecording(10, 10); |
| auto pic = rec.finishRecordingAsPicture(); |
| REPORTER_ASSERT(reporter, pic); |
| |
| auto data = pic->serialize(); |
| REPORTER_ASSERT(reporter, data); |
| |
| auto pic2 = SkPicture::MakeFromData(data->data(), data->size()); |
| REPORTER_ASSERT(reporter, pic2); |
| } |
| |
| |
| DEF_TEST(Picture_drawsNothing, r) { |
| // Tests that pic->cullRect().isEmpty() is a good way to test a picture |
| // recorded with an R-tree draws nothing. |
| struct { |
| bool draws_nothing; |
| void (*fn)(SkCanvas*); |
| } cases[] = { |
| { true, [](SkCanvas* c) { } }, |
| { true, [](SkCanvas* c) { c->save(); c->restore(); } }, |
| { true, [](SkCanvas* c) { c->save(); c->clipRect({0,0,5,5}); c->restore(); } }, |
| { true, [](SkCanvas* c) { c->clipRect({0,0,5,5}); } }, |
| |
| { false, [](SkCanvas* c) { c->drawRect({0,0,5,5}, SkPaint{}); } }, |
| { false, [](SkCanvas* c) { c->save(); c->drawRect({0,0,5,5}, SkPaint{}); c->restore(); } }, |
| { false, [](SkCanvas* c) { |
| c->drawRect({0,0, 5, 5}, SkPaint{}); |
| c->drawRect({5,5,10,10}, SkPaint{}); |
| }}, |
| }; |
| |
| for (const auto& c : cases) { |
| SkPictureRecorder rec; |
| SkRTreeFactory factory; |
| c.fn(rec.beginRecording(10,10, &factory)); |
| sk_sp<SkPicture> pic = rec.finishRecordingAsPicture(); |
| |
| REPORTER_ASSERT(r, pic->cullRect().isEmpty() == c.draws_nothing); |
| } |
| } |
| |
| DEF_TEST(Picture_emptyNestedPictureBug, r) { |
| const SkRect bounds = {-5000, -5000, 5000, 5000}; |
| |
| SkPictureRecorder recorder; |
| SkRTreeFactory factory; |
| |
| // These three pictures should all draw the same but due to bugs they don't: |
| // |
| // 1) inner has enough content that it is recoreded as an SkBigPicture, |
| // and all its content falls outside the positive/positive quadrant, |
| // and it is recorded with an R-tree so we contract the cullRect to those bounds; |
| // |
| // 2) middle wraps inner, |
| // and it its recorded with an R-tree so we update middle's cullRect to inner's; |
| // |
| // 3) outer wraps inner, |
| // and notices that middle contains only one op, drawPicture(inner), |
| // so it plays middle back during recording rather than ref'ing middle, |
| // querying middle's R-tree with its SkCanvas' bounds* {0,0, 5000,5000}, |
| // finding nothing to draw. |
| // |
| // * The bug was that these bounds were not tracked as {-5000,-5000, 5000,5000}. |
| { |
| SkCanvas* canvas = recorder.beginRecording(bounds, &factory); |
| canvas->translate(-100,-100); |
| canvas->drawRect({0,0,50,50}, SkPaint{}); |
| } |
| sk_sp<SkPicture> inner = recorder.finishRecordingAsPicture(); |
| |
| recorder.beginRecording(bounds, &factory)->drawPicture(inner); |
| sk_sp<SkPicture> middle = recorder.finishRecordingAsPicture(); |
| |
| // This doesn't need &factory to reproduce the bug, |
| // but it's nice to see we come up with the same {-100,-100, -50,-50} bounds. |
| recorder.beginRecording(bounds, &factory)->drawPicture(middle); |
| sk_sp<SkPicture> outer = recorder.finishRecordingAsPicture(); |
| |
| REPORTER_ASSERT(r, (inner ->cullRect() == SkRect{-100,-100, -50,-50})); |
| REPORTER_ASSERT(r, (middle->cullRect() == SkRect{-100,-100, -50,-50})); |
| REPORTER_ASSERT(r, (outer ->cullRect() == SkRect{-100,-100, -50,-50})); // Used to fail. |
| } |
| |
| DEF_TEST(Picture_fillsBBH, r) { |
| // Test empty (0 draws), mini (1 draw), and big (2+) pictures, making sure they fill the BBH. |
| const SkRect rects[] = { |
| { 0, 0, 20,20}, |
| {20,20, 40,40}, |
| }; |
| |
| for (int n = 0; n <= 2; n++) { |
| SkRTreeFactory factory; |
| SkPictureRecorder rec; |
| |
| sk_sp<SkBBoxHierarchy> bbh = factory(); |
| |
| SkCanvas* c = rec.beginRecording({0,0, 100,100}, bbh); |
| for (int i = 0; i < n; i++) { |
| c->drawRect(rects[i], SkPaint{}); |
| } |
| sk_sp<SkPicture> pic = rec.finishRecordingAsPicture(); |
| |
| std::vector<int> results; |
| bbh->search({0,0, 100,100}, &results); |
| REPORTER_ASSERT(r, (int)results.size() == n, |
| "results.size() == %d, want %d\n", (int)results.size(), n); |
| } |
| } |
| |
| DEF_TEST(Picture_nested_op_count, r) { |
| auto make_pic = [](int n, sk_sp<SkPicture> pic) { |
| SkPictureRecorder rec; |
| SkCanvas* c = rec.beginRecording({0,0, 100,100}); |
| for (int i = 0; i < n; i++) { |
| if (pic) { |
| c->drawPicture(pic); |
| } else { |
| c->drawRect({0,0, 100,100}, SkPaint{}); |
| } |
| } |
| return rec.finishRecordingAsPicture(); |
| }; |
| |
| auto check = [r](sk_sp<SkPicture> pic, int shallow, int nested) { |
| int s = pic->approximateOpCount(false); |
| int n = pic->approximateOpCount(true); |
| REPORTER_ASSERT(r, s == shallow); |
| REPORTER_ASSERT(r, n == nested); |
| }; |
| |
| sk_sp<SkPicture> leaf1 = make_pic(1, nullptr); |
| check(leaf1, 1, 1); |
| |
| sk_sp<SkPicture> leaf10 = make_pic(10, nullptr); |
| check(leaf10, 10, 10); |
| |
| check(make_pic( 1, leaf1), 1, 1); |
| check(make_pic( 1, leaf10), 1, 10); |
| check(make_pic(10, leaf1), 10, 10); |
| check(make_pic(10, leaf10), 10, 100); |
| } |