/* * Copyright 2014 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/SkCanvas.h" #include "include/core/SkPicture.h" #include "src/core/SkPictureFlat.h" #include "src/core/SkPictureRecord.h" #include "tests/Test.h" #include "tools/debugger/DebugCanvas.h" // This test exercises the Matrix/Clip State collapsing system. It generates // example skps and the compares the actual stored operations to the expected // operations. The test works by emitting canvas operations at three levels: // overall structure, bodies that draw something and model/clip state changes. // // Structure methods only directly emit save and restores but call the // ModelClip and Body helper methods to fill in the structure. Since they only // emit saves and restores the operations emitted by the structure methods will // be completely removed by the matrix/clip collapse. Note: every save in // a structure method is followed by a call to a ModelClip helper. // // Body methods only directly emit draw ops and saveLayer/restore pairs but call // the ModelClip helper methods. Since the body methods emit the ops that cannot // be collapsed (i.e., draw ops, saveLayer/restore) they also generate the // expected result information. Note: every saveLayer in a body method is // followed by a call to a ModelClip helper. // // The ModelClip methods output matrix and clip ops in various orders and // combinations. They contribute to the expected result by outputting the // expected matrix & clip ops. Note that, currently, the entire clip stack // is output for each MC state so the clip operations accumulate down the // save/restore stack. // TODOs: // check on clip offsets // - not sure if this is possible. The desire is to verify that the clip // operations' offsets point to the correct follow-on operations. This // could be difficult since there is no good way to communicate the // offset stored in the SkPicture to the debugger's clip objects // add comparison of rendered before & after images? // - not sure if this would be useful since it somewhat duplicates the // correctness test of running render_pictures in record mode and // rendering before and after images. Additionally the matrix/clip collapse // is sure to cause some small differences so an automated test might // yield too many false positives. // run the matrix/clip collapse system on the 10K skp set // - this should give us warm fuzzies that the matrix clip collapse // system is ready for prime time // bench the recording times with/without matrix/clip collapsing #ifdef SK_COLLAPSE_MATRIX_CLIP_STATE // Enable/disable debugging helper code //#define TEST_COLLAPSE_MATRIX_CLIP_STATE 1 // Extract the command ops from the input SkPicture static void gets_ops(SkPicture& input, SkTDArray* ops) { DebugCanvas debugCanvas(input.width(), input.height()); debugCanvas.setBounds(input.width(), input.height()); input.draw(&debugCanvas); ops->setCount(debugCanvas.getSize()); for (int i = 0; i < debugCanvas.getSize(); ++i) { (*ops)[i] = debugCanvas.getDrawCommandAt(i)->getType(); } } enum ClipType { kNone_ClipType, kRect_ClipType, kRRect_ClipType, kPath_ClipType, kRegion_ClipType, kLast_ClipType = kRRect_ClipType }; static const int kClipTypeCount = kLast_ClipType + 1; enum MatType { kNone_MatType, kTranslate_MatType, kScale_MatType, kSkew_MatType, kRotate_MatType, kConcat_MatType, kSetMatrix_MatType, kLast_MatType = kScale_MatType }; static const int kMatTypeCount = kLast_MatType + 1; // TODO: implement the rest of the draw ops enum DrawOpType { kNone_DrawOpType, #if 0 kBitmap_DrawOpType, kBitmapMatrix_DrawOpType, kBitmapNone_DrawOpType, kBitmapRectToRect_DrawOpType, #endif kClear_DrawOpType, #if 0 kData_DrawOpType, #endif kOval_DrawOpType, #if 0 kPaint_DrawOpType, kPath_DrawOpType, kPicture_DrawOpType, kPoints_DrawOpType, kPosText_DrawOpType, kPosTextTopBottom_DrawOpType, kPosTextH_DrawOpType, kPosTextHTopBottom_DrawOpType, #endif kRect_DrawOpType, kRRect_DrawOpType, #if 0 kSprite_DrawOpType, kText_DrawOpType, kTextOnPath_DrawOpType, kTextTopBottom_DrawOpType, kDrawVertices_DrawOpType, #endif kLastNonSaveLayer_DrawOpType = kRect_DrawOpType, // saveLayer's have to handled apart from the other draw operations // since they also alter the save/restore structure. kSaveLayer_DrawOpType, }; static const int kNonSaveLayerDrawOpTypeCount = kLastNonSaveLayer_DrawOpType + 1; typedef void (*PFEmitMC)(SkCanvas* canvas, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips); typedef void (*PFEmitBody)(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips); typedef void (*PFEmitStruct)(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, PFEmitBody emitBody, DrawOpType draw, SkTDArray* expected); ////////////////////////////////////////////////////////////////////////////// // TODO: expand the testing to include the different ops & AA types! static void emit_clip(SkCanvas* canvas, ClipType clip) { switch (clip) { case kNone_ClipType: break; case kRect_ClipType: { SkRect r = SkRect::MakeLTRB(10, 10, 90, 90); canvas->clipRect(r, SkRegion::kIntersect_Op, true); break; } case kRRect_ClipType: { SkRect r = SkRect::MakeLTRB(10, 10, 90, 90); SkRRect rr; rr.setRectXY(r, 10, 10); canvas->clipRRect(rr, SkRegion::kIntersect_Op, true); break; } case kPath_ClipType: { SkPath p; p.moveTo(5.0f, 5.0f); p.lineTo(50.0f, 50.0f); p.lineTo(100.0f, 5.0f); p.close(); canvas->clipPath(p, SkRegion::kIntersect_Op, true); break; } case kRegion_ClipType: { SkIRect rects[2] = { { 1, 1, 55, 55 }, { 45, 45, 99, 99 }, }; SkRegion r; r.setRects(rects, 2); canvas->clipRegion(r, SkRegion::kIntersect_Op); break; } default: SkASSERT(0); } } static void add_clip(ClipType clip, MatType mat, SkTDArray* expected) { if (nullptr == expected) { // expected is nullptr if this clip will be fused into later clips return; } switch (clip) { case kNone_ClipType: break; case kRect_ClipType: *expected->append() = CONCAT; *expected->append() = CLIP_RECT; break; case kRRect_ClipType: *expected->append() = CONCAT; *expected->append() = CLIP_RRECT; break; case kPath_ClipType: *expected->append() = CONCAT; *expected->append() = CLIP_PATH; break; case kRegion_ClipType: *expected->append() = CONCAT; *expected->append() = CLIP_REGION; break; default: SkASSERT(0); } } static void emit_mat(SkCanvas* canvas, MatType mat) { switch (mat) { case kNone_MatType: break; case kTranslate_MatType: canvas->translate(5.0f, 5.0f); break; case kScale_MatType: canvas->scale(1.1f, 1.1f); break; case kSkew_MatType: canvas->skew(1.1f, 1.1f); break; case kRotate_MatType: canvas->rotate(1.0f); break; case kConcat_MatType: { SkMatrix m; m.setTranslate(1.0f, 1.0f); canvas->concat(m); break; } case kSetMatrix_MatType: { SkMatrix m; m.setTranslate(1.0f, 1.0f); canvas->setMatrix(m); break; } default: SkASSERT(0); } } static void add_mat(MatType mat, SkTDArray* expected) { if (nullptr == expected) { // expected is nullptr if this matrix call will be fused into later ones return; } switch (mat) { case kNone_MatType: break; case kTranslate_MatType: // fall thru case kScale_MatType: // fall thru case kSkew_MatType: // fall thru case kRotate_MatType: // fall thru case kConcat_MatType: // fall thru case kSetMatrix_MatType: // TODO: this system currently converts a setMatrix to concat. If we wanted to // really preserve the setMatrix semantics we should keep it a setMatrix. I'm // not sure if this is a good idea though since this would keep things like pinch // zoom from working. *expected->append() = CONCAT; break; default: SkASSERT(0); } } static void emit_draw(SkCanvas* canvas, DrawOpType draw, SkTDArray* expected) { switch (draw) { case kNone_DrawOpType: break; case kClear_DrawOpType: canvas->clear(SK_ColorRED); *expected->append() = DRAW_CLEAR; break; case kOval_DrawOpType: { SkRect r = SkRect::MakeLTRB(10, 10, 90, 90); SkPaint p; canvas->drawOval(r, p); *expected->append() = DRAW_OVAL; break; } case kRect_DrawOpType: { SkRect r = SkRect::MakeLTRB(10, 10, 90, 90); SkPaint p; canvas->drawRect(r, p); *expected->append() = DRAW_RECT; break; } case kRRect_DrawOpType: { SkRect r = SkRect::MakeLTRB(10.0f, 10.0f, 90.0f, 90.0f); SkRRect rr; rr.setRectXY(r, 5.0f, 5.0f); SkPaint p; canvas->drawRRect(rr, p); *expected->append() = DRAW_RRECT; break; } default: SkASSERT(0); } } ////////////////////////////////////////////////////////////////////////////// // Emit: // clip // matrix // Simple case - the clip isn't effect by the matrix static void emit_clip_and_mat(SkCanvas* canvas, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { emit_clip(canvas, clip); emit_mat(canvas, mat); if (kNone_DrawOpType == draw) { return; } for (int i = 0; i < accumulatedClips; ++i) { add_clip(clip, mat, expected); } add_mat(mat, expected); } // Emit: // matrix // clip // Emitting the matrix first is more challenging since the matrix has to be // pushed across (i.e., applied to) the clip. static void emit_mat_and_clip(SkCanvas* canvas, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { emit_mat(canvas, mat); emit_clip(canvas, clip); if (kNone_DrawOpType == draw) { return; } // the matrix & clip order will be reversed once collapsed! for (int i = 0; i < accumulatedClips; ++i) { add_clip(clip, mat, expected); } add_mat(mat, expected); } // Emit: // matrix // clip // matrix // clip // This tests that the matrices and clips coalesce when collapsed static void emit_double_mat_and_clip(SkCanvas* canvas, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { emit_mat(canvas, mat); emit_clip(canvas, clip); emit_mat(canvas, mat); emit_clip(canvas, clip); if (kNone_DrawOpType == draw) { return; } for (int i = 0; i < accumulatedClips; ++i) { add_clip(clip, mat, expected); add_clip(clip, mat, expected); } add_mat(mat, expected); } // Emit: // matrix // clip // clip // This tests accumulation of clips in same transform state. It also tests pushing // of the matrix across both the clips. static void emit_mat_clip_clip(SkCanvas* canvas, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { emit_mat(canvas, mat); emit_clip(canvas, clip); emit_clip(canvas, clip); if (kNone_DrawOpType == draw) { return; } for (int i = 0; i < accumulatedClips; ++i) { add_clip(clip, mat, expected); add_clip(clip, mat, expected); } add_mat(mat, expected); } ////////////////////////////////////////////////////////////////////////////// // Emit: // matrix & clip calls // draw op static void emit_body0(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { bool needsSaveRestore = kNone_DrawOpType != draw && (kNone_MatType != mat || kNone_ClipType != clip); if (needsSaveRestore) { *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, draw, expected, accumulatedClips+1); emit_draw(canvas, draw, expected); if (needsSaveRestore) { *expected->append() = RESTORE; } } // Emit: // matrix & clip calls // draw op // matrix & clip calls // draw op static void emit_body1(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { bool needsSaveRestore = kNone_DrawOpType != draw && (kNone_MatType != mat || kNone_ClipType != clip); if (needsSaveRestore) { *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, draw, expected, accumulatedClips+1); emit_draw(canvas, draw, expected); if (needsSaveRestore) { *expected->append() = RESTORE; *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, draw, expected, accumulatedClips+2); emit_draw(canvas, draw, expected); if (needsSaveRestore) { *expected->append() = RESTORE; } } // Emit: // matrix & clip calls // SaveLayer // matrix & clip calls // draw op // Restore static void emit_body2(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { bool needsSaveRestore = kNone_DrawOpType != draw && (kNone_MatType != mat || kNone_ClipType != clip); if (kNone_MatType != mat || kNone_ClipType != clip) { *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, kSaveLayer_DrawOpType, expected, accumulatedClips+1); *expected->append() = SAVE_LAYER; // TODO: widen testing to exercise saveLayer's parameters canvas->saveLayer(nullptr, nullptr); if (needsSaveRestore) { *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, draw, expected, 1); emit_draw(canvas, draw, expected); if (needsSaveRestore) { *expected->append() = RESTORE; } canvas->restore(); *expected->append() = RESTORE; if (kNone_MatType != mat || kNone_ClipType != clip) { *expected->append() = RESTORE; } } // Emit: // matrix & clip calls // SaveLayer // matrix & clip calls // SaveLayer // matrix & clip calls // draw op // Restore // matrix & clip calls (will be ignored) // Restore static void emit_body3(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, DrawOpType draw, SkTDArray* expected, int accumulatedClips) { bool needsSaveRestore = kNone_DrawOpType != draw && (kNone_MatType != mat || kNone_ClipType != clip); if (kNone_MatType != mat || kNone_ClipType != clip) { *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, kSaveLayer_DrawOpType, expected, accumulatedClips+1); *expected->append() = SAVE_LAYER; // TODO: widen testing to exercise saveLayer's parameters canvas->saveLayer(nullptr, nullptr); (*emitMC)(canvas, mat, clip, kSaveLayer_DrawOpType, expected, 1); if (kNone_MatType != mat || kNone_ClipType != clip) { *expected->append() = SAVE; } *expected->append() = SAVE_LAYER; // TODO: widen testing to exercise saveLayer's parameters canvas->saveLayer(nullptr, nullptr); if (needsSaveRestore) { *expected->append() = SAVE; } (*emitMC)(canvas, mat, clip, draw, expected, 1); emit_draw(canvas, draw, expected); if (needsSaveRestore) { *expected->append() = RESTORE; } canvas->restore(); // for saveLayer *expected->append() = RESTORE; // for saveLayer if (kNone_MatType != mat || kNone_ClipType != clip) { *expected->append() = RESTORE; } canvas->restore(); // required to match forced SAVE_LAYER *expected->append() = RESTORE; if (kNone_MatType != mat || kNone_ClipType != clip) { *expected->append() = RESTORE; } } ////////////////////////////////////////////////////////////////////////////// // Emit: // Save // some body // Restore // Note: the outer save/restore are provided by beginRecording/endRecording static void emit_struct0(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, PFEmitBody emitBody, DrawOpType draw, SkTDArray* expected) { (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 0); } // Emit: // Save // matrix & clip calls // Save // some body // Restore // matrix & clip calls (will be ignored) // Restore // Note: the outer save/restore are provided by beginRecording/endRecording static void emit_struct1(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, PFEmitBody emitBody, DrawOpType draw, SkTDArray* expected) { (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these get fused into later ops canvas->save(); (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1); canvas->restore(); (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these will get removed } // Emit: // Save // matrix & clip calls // Save // some body // Restore // Save // some body // Restore // matrix & clip calls (will be ignored) // Restore // Note: the outer save/restore are provided by beginRecording/endRecording static void emit_struct2(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, PFEmitBody emitBody, DrawOpType draw, SkTDArray* expected) { (*emitMC)(canvas, mat, clip, draw, nullptr, 1); // these will get fused into later ops canvas->save(); (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1); canvas->restore(); canvas->save(); (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1); canvas->restore(); (*emitMC)(canvas, mat, clip, draw, nullptr, 1); // these will get removed } // Emit: // Save // matrix & clip calls // Save // some body // Restore // Save // matrix & clip calls // Save // some body // Restore // Restore // matrix & clip calls (will be ignored) // Restore // Note: the outer save/restore are provided by beginRecording/endRecording static void emit_struct3(SkCanvas* canvas, PFEmitMC emitMC, MatType mat, ClipType clip, PFEmitBody emitBody, DrawOpType draw, SkTDArray* expected) { (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these will get fused into later ops canvas->save(); (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1); canvas->restore(); canvas->save(); (*emitMC)(canvas, mat, clip, draw, nullptr, 1); // these will get fused into later ops canvas->save(); (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 2); canvas->restore(); canvas->restore(); (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these will get removed } ////////////////////////////////////////////////////////////////////////////// #ifdef SK_COLLAPSE_MATRIX_CLIP_STATE static void print(const SkTDArray& expected, const SkTDArray& actual) { SkDebugf("\n\nexpected %d --- actual %d\n", expected.count(), actual.count()); int max = std::max(expected.count(), actual.count()); for (int i = 0; i < max; ++i) { if (i < expected.count()) { SkDebugf("%16s, ", DrawCommand::GetCommandString(expected[i])); } else { SkDebugf("%16s, ", " "); } if (i < actual.count()) { SkDebugf("%s\n", DrawCommand::GetCommandString(actual[i])); } else { SkDebugf("\n"); } } SkDebugf("\n\n"); SkASSERT(0); } #endif static void test_collapse(skiatest::Reporter* reporter) { PFEmitStruct gStructure[] = { emit_struct0, emit_struct1, emit_struct2, emit_struct3 }; PFEmitBody gBody[] = { emit_body0, emit_body1, emit_body2, emit_body3 }; PFEmitMC gMCs[] = { emit_clip_and_mat, emit_mat_and_clip, emit_double_mat_and_clip, emit_mat_clip_clip }; for (size_t i = 0; i < SK_ARRAY_COUNT(gStructure); ++i) { for (size_t j = 0; j < SK_ARRAY_COUNT(gBody); ++j) { for (size_t k = 0; k < SK_ARRAY_COUNT(gMCs); ++k) { for (int l = 0; l < kMatTypeCount; ++l) { for (int m = 0; m < kClipTypeCount; ++m) { for (int n = 0; n < kNonSaveLayerDrawOpTypeCount; ++n) { #ifdef TEST_COLLAPSE_MATRIX_CLIP_STATE static int testID = -1; ++testID; if (testID < -1) { continue; } SkDebugf("test: %d\n", testID); #endif SkTDArray expected, actual; SkPicture picture; // Note: beginRecording/endRecording add a save/restore pair SkCanvas* canvas = picture.beginRecording(100, 100); (*gStructure[i])(canvas, gMCs[k], (MatType) l, (ClipType) m, gBody[j], (DrawOpType) n, &expected); picture.endRecording(); gets_ops(picture, &actual); REPORTER_ASSERT(reporter, expected.count() == actual.count()); if (expected.count() != actual.count()) { #ifdef TEST_COLLAPSE_MATRIX_CLIP_STATE print(expected, actual); #endif continue; } for (int i = 0; i < expected.count(); ++i) { REPORTER_ASSERT(reporter, expected[i] == actual[i]); #ifdef TEST_COLLAPSE_MATRIX_CLIP_STATE if (expected[i] != actual[i]) { print(expected, actual); } #endif break; } } } } } } } } DEF_TEST(MatrixClipCollapse, reporter) { test_collapse(reporter); } #endif