| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <utility> |
| |
| #include "flutter/display_list/display_list.h" |
| #include "flutter/display_list/display_list_canvas_dispatcher.h" |
| #include "flutter/display_list/display_list_canvas_recorder.h" |
| #include "flutter/display_list/display_list_comparable.h" |
| #include "flutter/display_list/display_list_flags.h" |
| #include "flutter/display_list/display_list_sampling_options.h" |
| #include "flutter/display_list/testing/dl_test_surface_provider.h" |
| #include "flutter/fml/math.h" |
| #include "flutter/testing/testing.h" |
| #include "third_party/skia/include/core/SkPictureRecorder.h" |
| #include "third_party/skia/include/core/SkSurface.h" |
| #include "third_party/skia/include/effects/SkBlenders.h" |
| #include "third_party/skia/include/effects/SkDashPathEffect.h" |
| #include "third_party/skia/include/effects/SkDiscretePathEffect.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| #include "third_party/skia/include/effects/SkImageFilters.h" |
| |
| namespace flutter { |
| namespace testing { |
| |
| using ClipOp = DlCanvas::ClipOp; |
| using PointMode = DlCanvas::PointMode; |
| |
| constexpr int kTestWidth = 200; |
| constexpr int kTestHeight = 200; |
| constexpr int kRenderWidth = 100; |
| constexpr int kRenderHeight = 100; |
| constexpr int kRenderHalfWidth = 50; |
| constexpr int kRenderHalfHeight = 50; |
| constexpr int kRenderLeft = (kTestWidth - kRenderWidth) / 2; |
| constexpr int kRenderTop = (kTestHeight - kRenderHeight) / 2; |
| constexpr int kRenderRight = kRenderLeft + kRenderWidth; |
| constexpr int kRenderBottom = kRenderTop + kRenderHeight; |
| constexpr int kRenderCenterX = (kRenderLeft + kRenderRight) / 2; |
| constexpr int kRenderCenterY = (kRenderTop + kRenderBottom) / 2; |
| constexpr SkScalar kRenderRadius = std::min(kRenderWidth, kRenderHeight) / 2.0; |
| constexpr SkScalar kRenderCornerRadius = kRenderRadius / 5.0; |
| |
| constexpr SkPoint kTestCenter = SkPoint::Make(kTestWidth / 2, kTestHeight / 2); |
| constexpr SkRect kTestBounds = SkRect::MakeWH(kTestWidth, kTestHeight); |
| constexpr SkRect kRenderBounds = |
| SkRect::MakeLTRB(kRenderLeft, kRenderTop, kRenderRight, kRenderBottom); |
| |
| // The tests try 3 miter limit values, 0.0, 4.0 (the default), and 10.0 |
| // These values will allow us to construct a diamond that spans the |
| // width or height of the render box and still show the miter for 4.0 |
| // and 10.0. |
| // These values were discovered by drawing a diamond path in Skia fiddle |
| // and then playing with the cross-axis size until the miter was about |
| // as large as it could get before it got cut off. |
| |
| // The X offsets which will be used for tall vertical diamonds are |
| // expressed in terms of the rendering height to obtain the proper angle |
| constexpr SkScalar kMiterExtremeDiamondOffsetX = kRenderHeight * 0.04; |
| constexpr SkScalar kMiter10DiamondOffsetX = kRenderHeight * 0.051; |
| constexpr SkScalar kMiter4DiamondOffsetX = kRenderHeight * 0.14; |
| |
| // The Y offsets which will be used for long horizontal diamonds are |
| // expressed in terms of the rendering width to obtain the proper angle |
| constexpr SkScalar kMiterExtremeDiamondOffsetY = kRenderWidth * 0.04; |
| constexpr SkScalar kMiter10DiamondOffsetY = kRenderWidth * 0.051; |
| constexpr SkScalar kMiter4DiamondOffsetY = kRenderWidth * 0.14; |
| |
| // Render 3 vertical and horizontal diamonds each |
| // designed to break at the tested miter limits |
| // 0.0, 4.0 and 10.0 |
| // Center is biased by 0.5 to include more pixel centers in the |
| // thin miters |
| constexpr SkScalar kXOffset0 = kRenderCenterX + 0.5; |
| constexpr SkScalar kXOffsetL1 = kXOffset0 - kMiter4DiamondOffsetX; |
| constexpr SkScalar kXOffsetL2 = kXOffsetL1 - kMiter10DiamondOffsetX; |
| constexpr SkScalar kXOffsetL3 = kXOffsetL2 - kMiter10DiamondOffsetX; |
| constexpr SkScalar kXOffsetR1 = kXOffset0 + kMiter4DiamondOffsetX; |
| constexpr SkScalar kXOffsetR2 = kXOffsetR1 + kMiterExtremeDiamondOffsetX; |
| constexpr SkScalar kXOffsetR3 = kXOffsetR2 + kMiterExtremeDiamondOffsetX; |
| constexpr SkPoint kVerticalMiterDiamondPoints[] = { |
| // Vertical diamonds: |
| // M10 M4 Mextreme |
| // /\ /|\ /\ top of RenderBounds |
| // / \ / | \ / \ to |
| // <----X--+--X----> RenderCenter |
| // \ / \ | / \ / to |
| // \/ \|/ \/ bottom of RenderBounds |
| // clang-format off |
| SkPoint::Make(kXOffsetL3, kRenderCenterY), |
| SkPoint::Make(kXOffsetL2, kRenderTop), |
| SkPoint::Make(kXOffsetL1, kRenderCenterY), |
| SkPoint::Make(kXOffset0, kRenderTop), |
| SkPoint::Make(kXOffsetR1, kRenderCenterY), |
| SkPoint::Make(kXOffsetR2, kRenderTop), |
| SkPoint::Make(kXOffsetR3, kRenderCenterY), |
| SkPoint::Make(kXOffsetR2, kRenderBottom), |
| SkPoint::Make(kXOffsetR1, kRenderCenterY), |
| SkPoint::Make(kXOffset0, kRenderBottom), |
| SkPoint::Make(kXOffsetL1, kRenderCenterY), |
| SkPoint::Make(kXOffsetL2, kRenderBottom), |
| SkPoint::Make(kXOffsetL3, kRenderCenterY), |
| // clang-format on |
| }; |
| const int kVerticalMiterDiamondPointCount = |
| sizeof(kVerticalMiterDiamondPoints) / |
| sizeof(kVerticalMiterDiamondPoints[0]); |
| |
| constexpr SkScalar kYOffset0 = kRenderCenterY + 0.5; |
| constexpr SkScalar kYOffsetU1 = kXOffset0 - kMiter4DiamondOffsetY; |
| constexpr SkScalar kYOffsetU2 = kYOffsetU1 - kMiter10DiamondOffsetY; |
| constexpr SkScalar kYOffsetU3 = kYOffsetU2 - kMiter10DiamondOffsetY; |
| constexpr SkScalar kYOffsetD1 = kXOffset0 + kMiter4DiamondOffsetY; |
| constexpr SkScalar kYOffsetD2 = kYOffsetD1 + kMiterExtremeDiamondOffsetY; |
| constexpr SkScalar kYOffsetD3 = kYOffsetD2 + kMiterExtremeDiamondOffsetY; |
| const SkPoint kHorizontalMiterDiamondPoints[] = { |
| // Horizontal diamonds |
| // Same configuration as Vertical diamonds above but |
| // rotated 90 degrees |
| // clang-format off |
| SkPoint::Make(kRenderCenterX, kYOffsetU3), |
| SkPoint::Make(kRenderLeft, kYOffsetU2), |
| SkPoint::Make(kRenderCenterX, kYOffsetU1), |
| SkPoint::Make(kRenderLeft, kYOffset0), |
| SkPoint::Make(kRenderCenterX, kYOffsetD1), |
| SkPoint::Make(kRenderLeft, kYOffsetD2), |
| SkPoint::Make(kRenderCenterX, kYOffsetD3), |
| SkPoint::Make(kRenderRight, kYOffsetD2), |
| SkPoint::Make(kRenderCenterX, kYOffsetD1), |
| SkPoint::Make(kRenderRight, kYOffset0), |
| SkPoint::Make(kRenderCenterX, kYOffsetU1), |
| SkPoint::Make(kRenderRight, kYOffsetU2), |
| SkPoint::Make(kRenderCenterX, kYOffsetU3), |
| // clang-format on |
| }; |
| const int kHorizontalMiterDiamondPointCount = |
| (sizeof(kHorizontalMiterDiamondPoints) / |
| sizeof(kHorizontalMiterDiamondPoints[0])); |
| |
| // A class to specify how much tolerance to allow in bounds estimates. |
| // For some attributes, the machinery must make some conservative |
| // assumptions as to the extent of the bounds, but some of our test |
| // parameters do not produce bounds that expand by the full conservative |
| // estimates. This class provides a number of tweaks to apply to the |
| // pixel bounds to account for the conservative factors. |
| // |
| // An instance is passed along through the methods and if any test adds |
| // a paint attribute or other modifier that will cause a more conservative |
| // estimate for bounds, it can modify the factors here to account for it. |
| // Ideally, all tests will be executed with geometry that will trigger |
| // the conservative cases anyway and all attributes will be combined with |
| // other attributes that make their output more predictable, but in those |
| // cases where a given test sequence cannot really provide attributes to |
| // demonstrate the worst case scenario, they can modify these factors to |
| // avoid false bounds overflow notifications. |
| class BoundsTolerance { |
| public: |
| BoundsTolerance() = default; |
| BoundsTolerance(const BoundsTolerance&) = default; |
| |
| BoundsTolerance addBoundsPadding(SkScalar bounds_pad_x, |
| SkScalar bounds_pad_y) const { |
| BoundsTolerance copy = BoundsTolerance(*this); |
| copy.bounds_pad_.offset(bounds_pad_x, bounds_pad_y); |
| return copy; |
| } |
| |
| BoundsTolerance mulScale(SkScalar scale_x, SkScalar scale_y) const { |
| BoundsTolerance copy = BoundsTolerance(*this); |
| copy.scale_.fX *= scale_x; |
| copy.scale_.fY *= scale_y; |
| return copy; |
| } |
| |
| BoundsTolerance addAbsolutePadding(SkScalar absolute_pad_x, |
| SkScalar absolute_pad_y) const { |
| BoundsTolerance copy = BoundsTolerance(*this); |
| copy.absolute_pad_.offset(absolute_pad_x, absolute_pad_y); |
| return copy; |
| } |
| |
| BoundsTolerance addPostClipPadding(SkScalar absolute_pad_x, |
| SkScalar absolute_pad_y) const { |
| BoundsTolerance copy = BoundsTolerance(*this); |
| copy.clip_pad_.offset(absolute_pad_x, absolute_pad_y); |
| return copy; |
| } |
| |
| BoundsTolerance addDiscreteOffset(SkScalar discrete_offset) const { |
| BoundsTolerance copy = BoundsTolerance(*this); |
| copy.discrete_offset_ += discrete_offset; |
| return copy; |
| } |
| |
| BoundsTolerance clip(SkRect clip) const { |
| BoundsTolerance copy = BoundsTolerance(*this); |
| if (!copy.clip_.intersect(clip)) { |
| copy.clip_.setEmpty(); |
| } |
| return copy; |
| } |
| |
| static SkRect Scale(const SkRect& rect, const SkPoint& scales) { |
| SkScalar outset_x = rect.width() * (scales.fX - 1); |
| SkScalar outset_y = rect.height() * (scales.fY - 1); |
| return rect.makeOutset(outset_x, outset_y); |
| } |
| |
| bool overflows(SkIRect pix_bounds, |
| int worst_bounds_pad_x, |
| int worst_bounds_pad_y) const { |
| SkRect allowed = SkRect::Make(pix_bounds); |
| allowed.outset(bounds_pad_.fX, bounds_pad_.fY); |
| allowed = Scale(allowed, scale_); |
| allowed.outset(absolute_pad_.fX, absolute_pad_.fY); |
| if (!allowed.intersect(clip_)) { |
| allowed.setEmpty(); |
| } |
| allowed.outset(clip_pad_.fX, clip_pad_.fY); |
| SkIRect rounded = allowed.roundOut(); |
| int pad_left = std::max(0, pix_bounds.fLeft - rounded.fLeft); |
| int pad_top = std::max(0, pix_bounds.fTop - rounded.fTop); |
| int pad_right = std::max(0, pix_bounds.fRight - rounded.fRight); |
| int pad_bottom = std::max(0, pix_bounds.fBottom - rounded.fBottom); |
| int allowed_pad_x = std::max(pad_left, pad_right); |
| int allowed_pad_y = std::max(pad_top, pad_bottom); |
| if (worst_bounds_pad_x > allowed_pad_x || |
| worst_bounds_pad_y > allowed_pad_y) { |
| FML_LOG(ERROR) << "acceptable bounds padding: " // |
| << allowed_pad_x << ", " << allowed_pad_y; |
| } |
| return (worst_bounds_pad_x > allowed_pad_x || |
| worst_bounds_pad_y > allowed_pad_y); |
| } |
| |
| SkScalar discrete_offset() const { return discrete_offset_; } |
| |
| private: |
| SkPoint bounds_pad_ = {0, 0}; |
| SkPoint scale_ = {1, 1}; |
| SkPoint absolute_pad_ = {0, 0}; |
| SkRect clip_ = {-1E9, -1E9, 1E9, 1E9}; |
| SkPoint clip_pad_ = {0, 0}; |
| |
| SkScalar discrete_offset_ = 0; |
| }; |
| |
| typedef const std::function<void(SkCanvas*, SkPaint&)> CvSetup; |
| typedef const std::function<void(SkCanvas*, const SkPaint&)> CvRenderer; |
| typedef const std::function<void(DisplayListBuilder&)> DlRenderer; |
| static const CvSetup kEmptyCvSetup = [](SkCanvas*, SkPaint&) {}; |
| static const CvRenderer kEmptyCvRenderer = [](SkCanvas*, const SkPaint&) {}; |
| static const DlRenderer kEmptyDlRenderer = [](DisplayListBuilder&) {}; |
| |
| using PixelFormat = DlSurfaceProvider::PixelFormat; |
| using BackendType = DlSurfaceProvider::BackendType; |
| |
| class RenderResult { |
| public: |
| RenderResult(const sk_sp<SkSurface>& surface, |
| const SkMatrix& matrix, |
| const SkIRect& clip_bounds) |
| : matrix_(matrix), clip_bounds_(clip_bounds) { |
| SkImageInfo info = surface->imageInfo(); |
| info = SkImageInfo::MakeN32Premul(info.dimensions()); |
| addr_ = malloc(info.computeMinByteSize() * info.height()); |
| pixmap_.reset(info, addr_, info.minRowBytes()); |
| EXPECT_TRUE(surface->readPixels(pixmap_, 0, 0)); |
| } |
| ~RenderResult() { free(addr_); } |
| |
| int width() const { return pixmap_.width(); } |
| int height() const { return pixmap_.height(); } |
| const SkMatrix& matrix() const { return matrix_; } |
| const SkIRect& clip_bounds() const { return clip_bounds_; } |
| const uint32_t* addr32(int x, int y) const { return pixmap_.addr32(x, y); } |
| |
| private: |
| SkPixmap pixmap_; |
| void* addr_ = nullptr; |
| SkMatrix matrix_; |
| SkIRect clip_bounds_; |
| }; |
| |
| struct RenderJobInfo { |
| int width = kTestWidth; |
| int height = kTestHeight; |
| DlColor bg = DlColor::kTransparent(); |
| SkScalar scale = SK_Scalar1; |
| SkScalar opacity = SK_Scalar1; |
| }; |
| |
| struct JobRenderer { |
| virtual void Render(SkCanvas* canvas, |
| const RenderJobInfo& info, |
| SkMatrix* setup_matrix = nullptr, |
| SkIRect* setup_clip_bounds = nullptr) = 0; |
| }; |
| |
| struct CvJobRenderer : public JobRenderer { |
| explicit CvJobRenderer(CvSetup& cv_setup = kEmptyCvSetup, |
| CvRenderer& cv_render = kEmptyCvRenderer, |
| CvRenderer& cv_restore = kEmptyCvRenderer) |
| : cv_setup_(cv_setup), cv_render_(cv_render), cv_restore_(cv_restore) {} |
| |
| void Render(SkCanvas* canvas, |
| const RenderJobInfo& info, |
| SkMatrix* setup_matrix = nullptr, |
| SkIRect* setup_clip_bounds = nullptr) override { |
| FML_DCHECK(info.opacity == SK_Scalar1); |
| SkPaint paint; |
| cv_setup_(canvas, paint); |
| setup_paint_ = paint; |
| paint_is_setup_ = true; |
| if (setup_matrix) { |
| *setup_matrix = canvas->getTotalMatrix(); |
| } |
| if (setup_clip_bounds) { |
| *setup_clip_bounds = canvas->getDeviceClipBounds(); |
| } |
| cv_render_(canvas, paint); |
| cv_restore_(canvas, paint); |
| } |
| |
| sk_sp<SkPicture> MakePicture() { |
| SkPictureRecorder recorder; |
| SkRTreeFactory rtree_factory; |
| SkCanvas* cv = recorder.beginRecording(kTestBounds, &rtree_factory); |
| Render(cv, {}, nullptr, nullptr); |
| return recorder.finishRecordingAsPicture(); |
| } |
| |
| SkPaint& setup_paint() { |
| FML_CHECK(paint_is_setup_); |
| return setup_paint_; |
| } |
| |
| private: |
| CvSetup cv_setup_; |
| CvRenderer cv_render_; |
| CvRenderer cv_restore_; |
| bool paint_is_setup_ = false; |
| SkPaint setup_paint_; |
| }; |
| |
| struct CvPictureRenderJob : public JobRenderer { |
| explicit CvPictureRenderJob(sk_sp<SkPicture> picture) |
| : picture_(std::move(picture)) {} |
| |
| void Render(SkCanvas* canvas, |
| const RenderJobInfo& info, |
| SkMatrix* setup_matrix = nullptr, |
| SkIRect* setup_clip_bounds = nullptr) { |
| FML_DCHECK(info.opacity == SK_Scalar1); |
| picture_->playback(canvas); |
| } |
| |
| private: |
| sk_sp<SkPicture> picture_; |
| }; |
| |
| struct DlRenderJob : public JobRenderer { |
| explicit DlRenderJob(DlRenderer& dl_setup = kEmptyDlRenderer, |
| DlRenderer& dl_render = kEmptyDlRenderer, |
| DlRenderer& dl_restore = kEmptyDlRenderer) |
| : dl_setup_(dl_setup), |
| dl_render_(dl_render), |
| dl_restore_(dl_restore), |
| is_built_(false) {} |
| |
| explicit DlRenderJob(sk_sp<DisplayList> display_list) |
| : dl_setup_(kEmptyDlRenderer), |
| dl_render_(kEmptyDlRenderer), |
| dl_restore_(kEmptyDlRenderer), |
| is_built_(true), |
| display_list_(std::move(display_list)) {} |
| |
| void Render(SkCanvas* canvas, |
| const RenderJobInfo& info, |
| SkMatrix* setup_matrix = nullptr, |
| SkIRect* setup_clip_bounds = nullptr) { |
| get_display_list(info, setup_matrix, setup_clip_bounds) |
| ->RenderTo(canvas, info.opacity); |
| } |
| |
| sk_sp<DisplayList> display_list() { |
| FML_CHECK(is_built_); |
| return display_list_; |
| } |
| |
| private: |
| sk_sp<DisplayList> get_display_list(const RenderJobInfo& info, |
| SkMatrix* setup_matrix, |
| SkIRect* setup_clip_bounds) { |
| if (!display_list_) { |
| DisplayListBuilder builder(SkRect::MakeWH(info.width, info.height)); |
| dl_setup_(builder); |
| if (setup_matrix) { |
| *setup_matrix = builder.GetTransform(); |
| } |
| if (setup_clip_bounds) { |
| *setup_clip_bounds = builder.GetDestinationClipBounds().roundOut(); |
| } |
| dl_render_(builder); |
| dl_restore_(builder); |
| display_list_ = builder.Build(); |
| is_built_ = true; |
| } |
| return display_list_; |
| } |
| |
| const DlRenderer dl_setup_; |
| const DlRenderer dl_render_; |
| const DlRenderer dl_restore_; |
| bool is_built_; |
| sk_sp<DisplayList> display_list_; |
| }; |
| |
| class RenderEnvironment { |
| public: |
| RenderEnvironment(const DlSurfaceProvider* provider, PixelFormat format) |
| : provider_(provider), format_(format) { |
| if (provider->supports(format)) { |
| surface_1x_ = |
| provider->MakeOffscreenSurface(kTestWidth, kTestHeight, format); |
| surface_2x_ = provider->MakeOffscreenSurface(kTestWidth * 2, |
| kTestHeight * 2, format); |
| } |
| } |
| |
| static RenderEnvironment Make565(const DlSurfaceProvider* provider) { |
| return RenderEnvironment(provider, PixelFormat::k565_PixelFormat); |
| } |
| |
| static RenderEnvironment MakeN32(const DlSurfaceProvider* provider) { |
| return RenderEnvironment(provider, PixelFormat::kN32Premul_PixelFormat); |
| } |
| |
| void init_ref(CvRenderer& cv_renderer, DlColor bg = DlColor::kTransparent()) { |
| init_ref(kEmptyCvSetup, cv_renderer, kEmptyDlRenderer, bg); |
| } |
| |
| void init_ref(CvSetup& cv_setup, |
| CvRenderer& cv_renderer, |
| DlRenderer& dl_setup, |
| DlColor bg = DlColor::kTransparent()) { |
| CvJobRenderer job(cv_setup, cv_renderer); |
| RenderJobInfo info = { |
| .bg = bg, |
| }; |
| ref_result_ = getResult(info, job); |
| dl_setup(ref_attr_); |
| } |
| |
| std::unique_ptr<RenderResult> getResult(const RenderJobInfo& info, |
| JobRenderer& renderer) const { |
| auto surface = getSurface(info.width, info.height); |
| FML_DCHECK(surface != nullptr); |
| auto canvas = surface->getCanvas(); |
| canvas->clear(info.bg); |
| |
| int restore_count = canvas->save(); |
| canvas->scale(info.scale, info.scale); |
| SkMatrix setup_matrix; |
| SkIRect setup_clip_bounds; |
| renderer.Render(canvas, info, &setup_matrix, &setup_clip_bounds); |
| canvas->restoreToCount(restore_count); |
| |
| canvas->flush(); |
| surface->flushAndSubmit(true); |
| return std::make_unique<RenderResult>(surface, setup_matrix, |
| setup_clip_bounds); |
| } |
| |
| std::unique_ptr<RenderResult> getResult(sk_sp<DisplayList> dl) const { |
| DlRenderJob job(std::move(dl)); |
| RenderJobInfo info = {}; |
| return getResult(info, job); |
| } |
| |
| const DlSurfaceProvider* provider() const { return provider_; } |
| bool valid() const { return provider_->supports(format_); } |
| const std::string backend_name() const { return provider_->backend_name(); } |
| |
| PixelFormat format() const { return format_; } |
| const DisplayListBuilder& ref_attr() const { return ref_attr_; } |
| const RenderResult* ref_result() const { return ref_result_.get(); } |
| |
| private: |
| sk_sp<SkSurface> getSurface(int width, int height) const { |
| FML_DCHECK(valid()); |
| FML_DCHECK(surface_1x_ != nullptr); |
| FML_DCHECK(surface_2x_ != nullptr); |
| if (width == kTestWidth && height == kTestHeight) { |
| return surface_1x_->sk_surface(); |
| } |
| if (width == kTestWidth * 2 && height == kTestHeight * 2) { |
| return surface_2x_->sk_surface(); |
| } |
| FML_LOG(ERROR) << "Test surface size (" << width << " x " << height |
| << ") not supported."; |
| FML_DCHECK(false); |
| return nullptr; |
| } |
| |
| const DlSurfaceProvider* provider_; |
| const PixelFormat format_; |
| std::shared_ptr<DlSurfaceInstance> surface_1x_; |
| std::shared_ptr<DlSurfaceInstance> surface_2x_; |
| |
| DisplayListBuilder ref_attr_; |
| std::unique_ptr<RenderResult> ref_result_; |
| }; |
| |
| class TestParameters { |
| public: |
| TestParameters(const CvRenderer& cv_renderer, |
| const DlRenderer& dl_renderer, |
| const DisplayListAttributeFlags& flags) |
| : cv_renderer_(cv_renderer), dl_renderer_(dl_renderer), flags_(flags) {} |
| |
| bool uses_paint() const { return !flags_.ignores_paint(); } |
| |
| bool should_match(const RenderEnvironment& env, |
| const DisplayListBuilder& attr, |
| const SkMatrix& matrix, |
| const SkIRect& device_clip, |
| bool has_diff_clip, |
| bool has_mutating_save_layer) const { |
| if (has_mutating_save_layer) { |
| return false; |
| } |
| const RenderResult* ref_result = env.ref_result(); |
| if (ref_result->clip_bounds() != device_clip || has_diff_clip) { |
| return false; |
| } |
| if (ref_result->matrix() != matrix && !flags_.is_flood()) { |
| return false; |
| } |
| if (flags_.ignores_paint()) { |
| return true; |
| } |
| const DisplayListBuilder& ref_attr = env.ref_attr(); |
| if (flags_.applies_anti_alias() && // |
| ref_attr.isAntiAlias() != attr.isAntiAlias()) { |
| return false; |
| } |
| if (flags_.applies_dither() && // |
| ref_attr.isDither() != attr.isDither()) { |
| return false; |
| } |
| if (flags_.applies_color() && // |
| ref_attr.getColor() != attr.getColor()) { |
| return false; |
| } |
| if (flags_.applies_blend() && // |
| ref_attr.getBlender() != attr.getBlender()) { |
| return false; |
| } |
| if (flags_.applies_color_filter() && // |
| (ref_attr.isInvertColors() != attr.isInvertColors() || |
| NotEquals(ref_attr.getColorFilter(), attr.getColorFilter()))) { |
| return false; |
| } |
| if (flags_.applies_mask_filter() && // |
| NotEquals(ref_attr.getMaskFilter(), attr.getMaskFilter())) { |
| return false; |
| } |
| if (flags_.applies_image_filter() && // |
| ref_attr.getImageFilter() != attr.getImageFilter()) { |
| return false; |
| } |
| if (flags_.applies_shader() && // |
| NotEquals(ref_attr.getColorSource(), attr.getColorSource())) { |
| return false; |
| } |
| |
| DisplayListSpecialGeometryFlags geo_flags = |
| flags_.WithPathEffect(attr.getPathEffect().get()); |
| if (flags_.applies_path_effect() && // |
| ref_attr.getPathEffect() != attr.getPathEffect()) { |
| if (attr.getPathEffect()->asDash() == nullptr) { |
| return false; |
| } |
| if (!ignores_dashes()) { |
| return false; |
| } |
| } |
| bool is_stroked = flags_.is_stroked(ref_attr.getStyle()); |
| if (flags_.is_stroked(attr.getStyle()) != is_stroked) { |
| return false; |
| } |
| if (!is_stroked) { |
| return true; |
| } |
| if (ref_attr.getStrokeWidth() != attr.getStrokeWidth()) { |
| return false; |
| } |
| if (geo_flags.may_have_end_caps() && // |
| getCap(ref_attr, geo_flags) != getCap(attr, geo_flags)) { |
| return false; |
| } |
| if (geo_flags.may_have_joins()) { |
| if (ref_attr.getStrokeJoin() != attr.getStrokeJoin()) { |
| return false; |
| } |
| if (ref_attr.getStrokeJoin() == DlStrokeJoin::kMiter) { |
| SkScalar ref_miter = ref_attr.getStrokeMiter(); |
| SkScalar test_miter = attr.getStrokeMiter(); |
| // miter limit < 1.4 affects right angles |
| if (geo_flags.may_have_acute_joins() || // |
| ref_miter < 1.4 || test_miter < 1.4) { |
| if (ref_miter != test_miter) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| DlStrokeCap getCap(const DisplayListBuilder& attr, |
| DisplayListSpecialGeometryFlags geo_flags) const { |
| DlStrokeCap cap = attr.getStrokeCap(); |
| if (geo_flags.butt_cap_becomes_square() && cap == DlStrokeCap::kButt) { |
| return DlStrokeCap::kSquare; |
| } |
| return cap; |
| } |
| |
| const BoundsTolerance adjust(const BoundsTolerance& tolerance, |
| const SkPaint& paint, |
| const SkMatrix& matrix) const { |
| if (is_draw_text_blob() && tolerance.discrete_offset() > 0) { |
| // drawTextBlob needs just a little more leeway when using a |
| // discrete path effect. |
| return tolerance.addBoundsPadding(2, 2); |
| } |
| if (is_draw_line()) { |
| return lineAdjust(tolerance, paint, matrix); |
| } |
| if (is_draw_arc_center()) { |
| if (paint.getStyle() != SkPaint::kFill_Style && |
| paint.getStrokeJoin() == SkPaint::kMiter_Join) { |
| // the miter join at the center of an arc does not really affect |
| // its bounds in any of our test cases, but the bounds code needs |
| // to take it into account for the cases where it might, so we |
| // relax our tolerance to reflect the miter bounds padding. |
| SkScalar miter_pad = |
| paint.getStrokeMiter() * paint.getStrokeWidth() * 0.5f; |
| return tolerance.addBoundsPadding(miter_pad, miter_pad); |
| } |
| } |
| return tolerance; |
| } |
| |
| const BoundsTolerance lineAdjust(const BoundsTolerance& tolerance, |
| const SkPaint& paint, |
| const SkMatrix& matrix) const { |
| SkScalar adjust = 0.0; |
| SkScalar half_width = paint.getStrokeWidth() * 0.5f; |
| if (tolerance.discrete_offset() > 0) { |
| // When a discrete path effect is added, the bounds calculations must |
| // allow for miters in any direction, but a horizontal line will not |
| // have miters in the horizontal direction, similarly for vertical |
| // lines, and diagonal lines will have miters off at a "45 degree" |
| // angle that don't expand the bounds much at all. |
| // Also, the discrete offset will not move any points parallel with |
| // the line, so provide tolerance for both miters and offset. |
| adjust = |
| half_width * paint.getStrokeMiter() + tolerance.discrete_offset(); |
| } |
| auto paint_effect = paint.refPathEffect(); |
| |
| DisplayListSpecialGeometryFlags geo_flags = |
| flags_.WithPathEffect(DlPathEffect::From(paint.refPathEffect()).get()); |
| if (paint.getStrokeCap() == SkPaint::kButt_Cap && |
| !geo_flags.butt_cap_becomes_square()) { |
| adjust = std::max(adjust, half_width); |
| } |
| if (adjust == 0) { |
| return tolerance; |
| } |
| SkScalar h_tolerance; |
| SkScalar v_tolerance; |
| if (is_horizontal_line()) { |
| FML_DCHECK(!is_vertical_line()); |
| h_tolerance = adjust; |
| v_tolerance = 0; |
| } else if (is_vertical_line()) { |
| h_tolerance = 0; |
| v_tolerance = adjust; |
| } else { |
| // The perpendicular miters just do not impact the bounds of |
| // diagonal lines at all as they are aimed in the wrong direction |
| // to matter. So allow tolerance in both axes. |
| h_tolerance = v_tolerance = adjust; |
| } |
| BoundsTolerance new_tolerance = |
| tolerance.addBoundsPadding(h_tolerance, v_tolerance); |
| return new_tolerance; |
| } |
| |
| const CvRenderer& cv_renderer() const { return cv_renderer_; } |
| void render_to(SkCanvas* canvas, SkPaint& paint) const { |
| cv_renderer_(canvas, paint); |
| } |
| |
| const DlRenderer& dl_renderer() const { return dl_renderer_; } |
| void render_to(DisplayListBuilder& builder) const { // |
| dl_renderer_(builder); |
| } |
| |
| // If a test is using any shadow operations then we cannot currently |
| // record those in an SkCanvas and play it back into a DisplayList |
| // because internally the operation gets encapsulated in a Skia |
| // ShadowRec which is not exposed by their headers. For operations |
| // that use shadows, we can perform a lot of tests, but not the tests |
| // that require SkCanvas->DisplayList transfers. |
| // See: https://bugs.chromium.org/p/skia/issues/detail?id=12125 |
| bool is_draw_shadows() const { return is_draw_shadows_; } |
| // Tests that call drawTextBlob with an sk_ref paint attribute will cause |
| // those attributes to be stored in an internal Skia cache so we need |
| // to expect that the |sk_ref.unique()| call will fail in those cases. |
| // See: (TBD(flar) - file Skia bug) |
| bool is_draw_text_blob() const { return is_draw_text_blob_; } |
| bool is_draw_display_list() const { return is_draw_display_list_; } |
| bool is_draw_line() const { return is_draw_line_; } |
| bool is_draw_arc_center() const { return is_draw_arc_center_; } |
| bool is_draw_path() const { return is_draw_path_; } |
| bool is_horizontal_line() const { return is_horizontal_line_; } |
| bool is_vertical_line() const { return is_vertical_line_; } |
| bool ignores_dashes() const { return ignores_dashes_; } |
| |
| TestParameters& set_draw_shadows() { |
| is_draw_shadows_ = true; |
| return *this; |
| } |
| TestParameters& set_draw_text_blob() { |
| is_draw_text_blob_ = true; |
| return *this; |
| } |
| TestParameters& set_draw_display_list() { |
| is_draw_display_list_ = true; |
| return *this; |
| } |
| TestParameters& set_draw_line() { |
| is_draw_line_ = true; |
| return *this; |
| } |
| TestParameters& set_draw_arc_center() { |
| is_draw_arc_center_ = true; |
| return *this; |
| } |
| TestParameters& set_draw_path() { |
| is_draw_path_ = true; |
| return *this; |
| } |
| TestParameters& set_ignores_dashes() { |
| ignores_dashes_ = true; |
| return *this; |
| } |
| TestParameters& set_horizontal_line() { |
| is_horizontal_line_ = true; |
| return *this; |
| } |
| TestParameters& set_vertical_line() { |
| is_vertical_line_ = true; |
| return *this; |
| } |
| |
| private: |
| const CvRenderer& cv_renderer_; |
| const DlRenderer& dl_renderer_; |
| const DisplayListAttributeFlags& flags_; |
| |
| bool is_draw_shadows_ = false; |
| bool is_draw_text_blob_ = false; |
| bool is_draw_display_list_ = false; |
| bool is_draw_line_ = false; |
| bool is_draw_arc_center_ = false; |
| bool is_draw_path_ = false; |
| bool ignores_dashes_ = false; |
| bool is_horizontal_line_ = false; |
| bool is_vertical_line_ = false; |
| }; |
| |
| class CaseParameters { |
| public: |
| explicit CaseParameters(std::string info) |
| : CaseParameters(std::move(info), kEmptyCvRenderer, kEmptyDlRenderer) {} |
| |
| CaseParameters(std::string info, CvSetup& cv_setup, DlRenderer& dl_setup) |
| : CaseParameters(std::move(info), |
| cv_setup, |
| dl_setup, |
| kEmptyCvRenderer, |
| kEmptyDlRenderer, |
| SK_ColorTRANSPARENT, |
| false, |
| false, |
| false) {} |
| |
| CaseParameters(std::string info, |
| CvSetup& cv_setup, |
| DlRenderer& dl_setup, |
| CvRenderer& cv_restore, |
| DlRenderer& dl_restore, |
| DlColor bg, |
| bool has_diff_clip, |
| bool has_mutating_save_layer, |
| bool fuzzy_compare_components) |
| : info_(std::move(info)), |
| bg_(bg), |
| cv_setup_(cv_setup), |
| dl_setup_(dl_setup), |
| cv_restore_(cv_restore), |
| dl_restore_(dl_restore), |
| has_diff_clip_(has_diff_clip), |
| has_mutating_save_layer_(has_mutating_save_layer), |
| fuzzy_compare_components_(fuzzy_compare_components) {} |
| |
| CaseParameters with_restore(CvRenderer& cv_restore, |
| DlRenderer& dl_restore, |
| bool mutating_layer, |
| bool fuzzy_compare_components = false) { |
| return CaseParameters(info_, cv_setup_, dl_setup_, cv_restore, dl_restore, |
| bg_, has_diff_clip_, mutating_layer, |
| fuzzy_compare_components); |
| } |
| |
| CaseParameters with_bg(DlColor bg) { |
| return CaseParameters(info_, cv_setup_, dl_setup_, cv_restore_, dl_restore_, |
| bg, has_diff_clip_, has_mutating_save_layer_, |
| fuzzy_compare_components_); |
| } |
| |
| CaseParameters with_diff_clip() { |
| return CaseParameters(info_, cv_setup_, dl_setup_, cv_restore_, dl_restore_, |
| bg_, true, has_mutating_save_layer_, |
| fuzzy_compare_components_); |
| } |
| |
| std::string info() const { return info_; } |
| DlColor bg() const { return bg_; } |
| bool has_diff_clip() const { return has_diff_clip_; } |
| bool has_mutating_save_layer() const { return has_mutating_save_layer_; } |
| bool fuzzy_compare_components() const { return fuzzy_compare_components_; } |
| |
| CvSetup cv_setup() const { return cv_setup_; } |
| DlRenderer dl_setup() const { return dl_setup_; } |
| CvRenderer cv_restore() const { return cv_restore_; } |
| DlRenderer dl_restore() const { return dl_restore_; } |
| |
| const SkPaint render_to(SkCanvas* canvas, // |
| const TestParameters& testP) const { |
| SkPaint paint; |
| cv_setup_(canvas, paint); |
| testP.render_to(canvas, paint); |
| cv_restore_(canvas, paint); |
| return paint; |
| } |
| |
| void render_to(DisplayListBuilder& builder, |
| const TestParameters& testP) const { |
| dl_setup_(builder); |
| testP.render_to(builder); |
| dl_restore_(builder); |
| } |
| |
| private: |
| const std::string info_; |
| const DlColor bg_; |
| const CvSetup cv_setup_; |
| const DlRenderer dl_setup_; |
| const CvRenderer cv_restore_; |
| const DlRenderer dl_restore_; |
| const bool has_diff_clip_; |
| const bool has_mutating_save_layer_; |
| const bool fuzzy_compare_components_; |
| }; |
| |
| class CanvasCompareTester { |
| public: |
| static std::vector<std::unique_ptr<DlSurfaceProvider>> kTestProviders; |
| |
| static BoundsTolerance DefaultTolerance; |
| |
| static void RenderAll(const TestParameters& params, |
| const BoundsTolerance& tolerance = DefaultTolerance) { |
| for (auto& provider : kTestProviders) { |
| RenderEnvironment env = RenderEnvironment::MakeN32(provider.get()); |
| env.init_ref(params.cv_renderer()); |
| RenderWithTransforms(params, env, tolerance); |
| RenderWithClips(params, env, tolerance); |
| RenderWithSaveRestore(params, env, tolerance); |
| // Only test attributes if the canvas version uses the paint object |
| if (params.uses_paint()) { |
| RenderWithAttributes(params, env, tolerance); |
| } |
| } |
| } |
| |
| static void RenderWithSaveRestore(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& tolerance) { |
| SkRect clip = |
| SkRect::MakeXYWH(kRenderCenterX - 1, kRenderCenterY - 1, 2, 2); |
| SkRect rect = SkRect::MakeXYWH(kRenderCenterX, kRenderCenterY, 10, 10); |
| DlColor alpha_layer_color = DlColor::kCyan().withAlpha(0x7f); |
| DlColor default_color = DlPaint::kDefaultColor; |
| CvRenderer cv_safe_restore = [=](SkCanvas* cv, const SkPaint& p) { |
| // Draw another primitive to disable peephole optimizations |
| cv->drawRect(kRenderBounds.makeOffset(500, 500), p); |
| cv->restore(); |
| }; |
| DlRenderer dl_safe_restore = [=](DisplayListBuilder& b) { |
| // Draw another primitive to disable peephole optimizations |
| b.drawRect(kRenderBounds.makeOffset(500, 500)); |
| b.restore(); |
| }; |
| CvRenderer cv_opt_restore = [=](SkCanvas* cv, const SkPaint& p) { |
| // Just a simple restore to allow peephole optimizations to occur |
| cv->restore(); |
| }; |
| DlRenderer dl_opt_restore = [=](DisplayListBuilder& b) { |
| // Just a simple restore to allow peephole optimizations to occur |
| b.restore(); |
| }; |
| SkRect layer_bounds = kRenderBounds.makeInset(15, 15); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "With prior save/clip/restore", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv->save(); |
| cv->clipRect(clip, SkClipOp::kIntersect, false); |
| SkPaint p2; |
| cv->drawRect(rect, p2); |
| p2.setBlendMode(SkBlendMode::kClear); |
| cv->drawRect(rect, p2); |
| cv->restore(); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.save(); |
| b.clipRect(clip, ClipOp::kIntersect, false); |
| b.drawRect(rect); |
| b.setBlendMode(DlBlendMode::kClear); |
| b.drawRect(rect); |
| b.setBlendMode(DlBlendMode::kSrcOver); |
| b.restore(); |
| })); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer no paint, no bounds", |
| [=](SkCanvas* cv, SkPaint& p) { // |
| cv->saveLayer(nullptr, nullptr); |
| }, |
| [=](DisplayListBuilder& b) { // |
| b.saveLayer(nullptr, false); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, false)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer no paint, with bounds", |
| [=](SkCanvas* cv, SkPaint& p) { // |
| cv->saveLayer(layer_bounds, nullptr); |
| }, |
| [=](DisplayListBuilder& b) { // |
| b.saveLayer(&layer_bounds, false); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer with alpha, no bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setColor(alpha_layer_color); |
| cv->saveLayer(nullptr, &save_p); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColor(alpha_layer_color); |
| b.saveLayer(nullptr, true); |
| b.setColor(default_color); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer with peephole alpha, no bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setColor(alpha_layer_color); |
| cv->saveLayer(nullptr, &save_p); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColor(alpha_layer_color); |
| b.saveLayer(nullptr, true); |
| b.setColor(default_color); |
| }) |
| .with_restore(cv_opt_restore, dl_opt_restore, true, true)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer with alpha and bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setColor(alpha_layer_color); |
| cv->saveLayer(layer_bounds, &save_p); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColor(alpha_layer_color); |
| b.saveLayer(&layer_bounds, true); |
| b.setColor(default_color); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| { |
| // Being able to see a backdrop blur requires a non-default background |
| // so we create a new environment for these tests that has a checkerboard |
| // background that can be blurred by the backdrop filter. We also want |
| // to avoid the rendered primitive from obscuring the blurred background |
| // so we set an alpha value which works for all primitives except for |
| // drawColor which can override the alpha with its color, but it now uses |
| // a non-opaque color to avoid that problem. |
| RenderEnvironment backdrop_env = |
| RenderEnvironment::MakeN32(env.provider()); |
| CvSetup cv_backdrop_setup = [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint setup_p; |
| setup_p.setShader(kTestImageColorSource.skia_object()); |
| cv->drawPaint(setup_p); |
| p.setAlpha(p.getAlpha() / 2); |
| }; |
| DlRenderer dl_backdrop_setup = [=](DisplayListBuilder& b) { |
| b.setColorSource(&kTestImageColorSource); |
| b.drawPaint(); |
| b.setColorSource(nullptr); |
| DlColor current_color = b.getColor(); |
| b.setColor(current_color.withAlpha(current_color.getAlpha() / 2)); |
| }; |
| backdrop_env.init_ref(cv_backdrop_setup, testP.cv_renderer(), |
| dl_backdrop_setup); |
| |
| DlBlurImageFilter backdrop(5, 5, DlTileMode::kDecal); |
| RenderWith(testP, backdrop_env, tolerance, |
| CaseParameters( |
| "saveLayer with backdrop", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_backdrop_setup(cv, p); |
| cv->saveLayer(SkCanvas::SaveLayerRec( |
| nullptr, nullptr, backdrop.skia_object().get(), 0)); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_backdrop_setup(b); |
| b.saveLayer(nullptr, SaveLayerOptions::kNoAttributes, |
| &backdrop); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| RenderWith( |
| testP, backdrop_env, tolerance, |
| CaseParameters( |
| "saveLayer with bounds and backdrop", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_backdrop_setup(cv, p); |
| cv->saveLayer(SkCanvas::SaveLayerRec( |
| &layer_bounds, nullptr, backdrop.skia_object().get(), 0)); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_backdrop_setup(b); |
| b.saveLayer(&layer_bounds, SaveLayerOptions::kNoAttributes, |
| &backdrop); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, backdrop_env, tolerance, |
| CaseParameters( |
| "clipped saveLayer with backdrop", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_backdrop_setup(cv, p); |
| cv->clipRect(layer_bounds); |
| cv->saveLayer(SkCanvas::SaveLayerRec( |
| nullptr, nullptr, backdrop.skia_object().get(), 0)); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_backdrop_setup(b); |
| b.clipRect(layer_bounds, ClipOp::kIntersect, false); |
| b.saveLayer(nullptr, SaveLayerOptions::kNoAttributes, |
| &backdrop); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| } |
| |
| { |
| // clang-format off |
| constexpr float rotate_alpha_color_matrix[20] = { |
| 0, 1, 0, 0 , 0, |
| 0, 0, 1, 0 , 0, |
| 1, 0, 0, 0 , 0, |
| 0, 0, 0, 0.5, 0, |
| }; |
| // clang-format on |
| DlMatrixColorFilter filter(rotate_alpha_color_matrix); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ColorFilter, no bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setColorFilter(filter.skia_object()); |
| cv->saveLayer(nullptr, &save_p); |
| p.setStrokeWidth(5.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColorFilter(&filter); |
| b.saveLayer(nullptr, true); |
| b.setColorFilter(nullptr); |
| b.setStrokeWidth(5.0); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| } |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ColorFilter and bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setColorFilter(filter.skia_object()); |
| cv->saveLayer(kRenderBounds, &save_p); |
| p.setStrokeWidth(5.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColorFilter(&filter); |
| b.saveLayer(&kRenderBounds, true); |
| b.setColorFilter(nullptr); |
| b.setStrokeWidth(5.0); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| } |
| } |
| { |
| sk_sp<SkImageFilter> sk_filter = SkImageFilters::Arithmetic( |
| 0.1, 0.1, 0.1, 0.25, true, nullptr, nullptr); |
| DlUnknownImageFilter filter(sk_filter); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ImageFilter, no bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setImageFilter(filter.skia_object()); |
| cv->saveLayer(nullptr, &save_p); |
| p.setStrokeWidth(5.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setImageFilter(&filter); |
| b.saveLayer(nullptr, true); |
| b.setImageFilter(nullptr); |
| b.setStrokeWidth(5.0); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| } |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ImageFilter and bounds", |
| [=](SkCanvas* cv, SkPaint& p) { |
| SkPaint save_p; |
| save_p.setImageFilter(filter.skia_object()); |
| cv->saveLayer(kRenderBounds, &save_p); |
| p.setStrokeWidth(5.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setImageFilter(&filter); |
| b.saveLayer(&kRenderBounds, true); |
| b.setImageFilter(nullptr); |
| b.setStrokeWidth(5.0); |
| }) |
| .with_restore(cv_safe_restore, dl_safe_restore, true)); |
| } |
| } |
| } |
| |
| static void RenderWithAttributes(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& tolerance) { |
| RenderWith(testP, env, tolerance, CaseParameters("Defaults Test")); |
| |
| { |
| // CPU renderer with default line width of 0 does not show antialiasing |
| // for stroked primitives, so we make a new reference with a non-trivial |
| // stroke width to demonstrate the differences |
| RenderEnvironment aa_env = RenderEnvironment::MakeN32(env.provider()); |
| // Tweak the bounds tolerance for the displacement of 1/10 of a pixel |
| const BoundsTolerance aa_tolerance = tolerance.addBoundsPadding(1, 1); |
| CvSetup cv_aa_setup = [=](SkCanvas* cv, SkPaint& p) { |
| cv->translate(0.1, 0.1); |
| p.setStrokeWidth(5.0); |
| }; |
| DlRenderer dl_aa_setup = [=](DisplayListBuilder& b) { |
| b.translate(0.1, 0.1); |
| b.setStrokeWidth(5.0); |
| }; |
| aa_env.init_ref(cv_aa_setup, testP.cv_renderer(), dl_aa_setup); |
| RenderWith(testP, aa_env, aa_tolerance, |
| CaseParameters( |
| "AntiAlias == True", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_aa_setup(cv, p); |
| p.setAntiAlias(true); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_aa_setup(b); |
| b.setAntiAlias(true); |
| })); |
| RenderWith(testP, aa_env, aa_tolerance, |
| CaseParameters( |
| "AntiAlias == False", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_aa_setup(cv, p); |
| p.setAntiAlias(false); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_aa_setup(b); |
| b.setAntiAlias(false); |
| })); |
| } |
| |
| { |
| // The CPU renderer does not always dither for solid colors and we |
| // need to use a non-default color (default is black) on an opaque |
| // surface, so we use a shader instead of a color. Also, thin stroked |
| // primitives (mainly drawLine and drawPoints) do not show much |
| // dithering so we use a non-trivial stroke width as well. |
| RenderEnvironment dither_env = RenderEnvironment::Make565(env.provider()); |
| if (!dither_env.valid()) { |
| // Currently only happens on Metal backend |
| static std::set<std::string> warnings_sent; |
| std::string name = dither_env.backend_name(); |
| if (warnings_sent.find(name) == warnings_sent.end()) { |
| warnings_sent.insert(name); |
| FML_LOG(INFO) << "Skipping Dithering tests on " << name; |
| } |
| } else { |
| DlColor dither_bg = DlColor::kBlack(); |
| CvSetup cv_dither_setup = [=](SkCanvas*, SkPaint& p) { |
| p.setShader(kTestImageColorSource.skia_object()); |
| p.setAlpha(0xf0); |
| p.setStrokeWidth(5.0); |
| }; |
| DlRenderer dl_dither_setup = [=](DisplayListBuilder& b) { |
| b.setColorSource(&kTestImageColorSource); |
| b.setColor(DlColor(0xf0000000)); |
| b.setStrokeWidth(5.0); |
| }; |
| dither_env.init_ref(cv_dither_setup, testP.cv_renderer(), // |
| dl_dither_setup, dither_bg); |
| RenderWith(testP, dither_env, tolerance, |
| CaseParameters( |
| "Dither == True", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_dither_setup(cv, p); |
| p.setDither(true); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_dither_setup(b); |
| b.setDither(true); |
| }) |
| .with_bg(dither_bg)); |
| RenderWith(testP, dither_env, tolerance, |
| CaseParameters( |
| "Dither = False", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_dither_setup(cv, p); |
| p.setDither(false); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_dither_setup(b); |
| b.setDither(false); |
| }) |
| .with_bg(dither_bg)); |
| } |
| } |
| |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Color == Blue", |
| [=](SkCanvas*, SkPaint& p) { p.setColor(SK_ColorBLUE); }, |
| [=](DisplayListBuilder& b) { b.setColor(SK_ColorBLUE); })); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Color == Green", |
| [=](SkCanvas*, SkPaint& p) { p.setColor(SK_ColorGREEN); }, |
| [=](DisplayListBuilder& b) { b.setColor(SK_ColorGREEN); })); |
| |
| RenderWithStrokes(testP, env, tolerance); |
| |
| { |
| // half opaque cyan |
| DlColor blendable_color = DlColor::kCyan().withAlpha(0x7f); |
| DlColor bg = DlColor::kWhite(); |
| |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Blend == SrcIn", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setBlendMode(SkBlendMode::kSrcIn); |
| p.setColor(blendable_color); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setBlendMode(DlBlendMode::kSrcIn); |
| b.setColor(blendable_color); |
| }) |
| .with_bg(bg)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Blend == DstIn", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setBlendMode(SkBlendMode::kDstIn); |
| p.setColor(blendable_color); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setBlendMode(DlBlendMode::kDstIn); |
| b.setColor(blendable_color); |
| }) |
| .with_bg(bg)); |
| } |
| |
| { |
| sk_sp<SkBlender> blender = |
| SkBlenders::Arithmetic(0.25, 0.25, 0.25, 0.25, false); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Blender == Arithmetic 0.25-false", |
| [=](SkCanvas*, SkPaint& p) { p.setBlender(blender); }, |
| [=](DisplayListBuilder& b) { b.setBlender(blender); })); |
| } |
| EXPECT_TRUE(blender->unique()) << "Blender Cleanup"; |
| blender = SkBlenders::Arithmetic(0.25, 0.25, 0.25, 0.25, true); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Blender == Arithmetic 0.25-true", |
| [=](SkCanvas*, SkPaint& p) { p.setBlender(blender); }, |
| [=](DisplayListBuilder& b) { b.setBlender(blender); })); |
| } |
| EXPECT_TRUE(blender->unique()) << "Blender Cleanup"; |
| } |
| |
| { |
| // Being able to see a blur requires some non-default attributes, |
| // like a non-trivial stroke width and a shader rather than a color |
| // (for drawPaint) so we create a new environment for these tests. |
| RenderEnvironment blur_env = RenderEnvironment::MakeN32(env.provider()); |
| CvSetup cv_blur_setup = [=](SkCanvas*, SkPaint& p) { |
| p.setShader(kTestImageColorSource.skia_object()); |
| p.setStrokeWidth(5.0); |
| }; |
| DlRenderer dl_blur_setup = [=](DisplayListBuilder& b) { |
| b.setColorSource(&kTestImageColorSource); |
| b.setStrokeWidth(5.0); |
| }; |
| blur_env.init_ref(cv_blur_setup, testP.cv_renderer(), dl_blur_setup); |
| DlBlurImageFilter filter_decal_5(5.0, 5.0, DlTileMode::kDecal); |
| BoundsTolerance blur_5_tolerance = tolerance.addBoundsPadding(4, 4); |
| { |
| RenderWith(testP, blur_env, blur_5_tolerance, |
| CaseParameters( |
| "ImageFilter == Decal Blur 5", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_blur_setup(cv, p); |
| p.setImageFilter(filter_decal_5.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_blur_setup(b); |
| b.setImageFilter(&filter_decal_5); |
| })); |
| } |
| DlBlurImageFilter filter_clamp_5(5.0, 5.0, DlTileMode::kClamp); |
| { |
| RenderWith(testP, blur_env, blur_5_tolerance, |
| CaseParameters( |
| "ImageFilter == Clamp Blur 5", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_blur_setup(cv, p); |
| p.setImageFilter(filter_clamp_5.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_blur_setup(b); |
| b.setImageFilter(&filter_clamp_5); |
| })); |
| } |
| } |
| |
| { |
| // Being able to see a dilate requires some non-default attributes, |
| // like a non-trivial stroke width and a shader rather than a color |
| // (for drawPaint) so we create a new environment for these tests. |
| RenderEnvironment dilate_env = RenderEnvironment::MakeN32(env.provider()); |
| CvSetup cv_dilate_setup = [=](SkCanvas*, SkPaint& p) { |
| p.setShader(kTestImageColorSource.skia_object()); |
| p.setStrokeWidth(5.0); |
| }; |
| DlRenderer dl_dilate_setup = [=](DisplayListBuilder& b) { |
| b.setColorSource(&kTestImageColorSource); |
| b.setStrokeWidth(5.0); |
| }; |
| dilate_env.init_ref(cv_dilate_setup, testP.cv_renderer(), |
| dl_dilate_setup); |
| DlDilateImageFilter filter_5(5.0, 5.0); |
| RenderWith(testP, dilate_env, tolerance, |
| CaseParameters( |
| "ImageFilter == Dilate 5", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_dilate_setup(cv, p); |
| p.setImageFilter(filter_5.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_dilate_setup(b); |
| b.setImageFilter(&filter_5); |
| })); |
| } |
| |
| { |
| // Being able to see an erode requires some non-default attributes, |
| // like a non-trivial stroke width and a shader rather than a color |
| // (for drawPaint) so we create a new environment for these tests. |
| RenderEnvironment erode_env = RenderEnvironment::MakeN32(env.provider()); |
| CvSetup cv_erode_setup = [=](SkCanvas*, SkPaint& p) { |
| p.setShader(kTestImageColorSource.skia_object()); |
| p.setStrokeWidth(6.0); |
| }; |
| DlRenderer dl_erode_setup = [=](DisplayListBuilder& b) { |
| b.setColorSource(&kTestImageColorSource); |
| b.setStrokeWidth(6.0); |
| }; |
| erode_env.init_ref(cv_erode_setup, testP.cv_renderer(), dl_erode_setup); |
| // do not erode too much, because some tests assert there are enough |
| // pixels that are changed. |
| DlErodeImageFilter filter_1(1.0, 1.0); |
| RenderWith(testP, erode_env, tolerance, |
| CaseParameters( |
| "ImageFilter == Erode 1", |
| [=](SkCanvas* cv, SkPaint& p) { |
| cv_erode_setup(cv, p); |
| p.setImageFilter(filter_1.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| dl_erode_setup(b); |
| b.setImageFilter(&filter_1); |
| })); |
| } |
| |
| { |
| // clang-format off |
| constexpr float rotate_color_matrix[20] = { |
| 0, 1, 0, 0, 0, |
| 0, 0, 1, 0, 0, |
| 1, 0, 0, 0, 0, |
| 0, 0, 0, 1, 0, |
| }; |
| constexpr float invert_color_matrix[20] = { |
| -1.0, 0, 0, 1.0, 0, |
| 0, -1.0, 0, 1.0, 0, |
| 0, 0, -1.0, 1.0, 0, |
| 1.0, 1.0, 1.0, 1.0, 0, |
| }; |
| // clang-format on |
| DlMatrixColorFilter filter(rotate_color_matrix); |
| { |
| DlColor bg = DlColor::kWhite(); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "ColorFilter == RotateRGB", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setColor(DlColor::kYellow()); |
| p.setColorFilter(filter.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColor(DlColor::kYellow()); |
| b.setColorFilter(&filter); |
| }) |
| .with_bg(bg)); |
| } |
| filter = DlMatrixColorFilter(invert_color_matrix); |
| { |
| DlColor bg = DlColor::kWhite(); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "ColorFilter == Invert", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setColor(DlColor::kYellow()); |
| p.setColorFilter(filter.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColor(DlColor::kYellow()); |
| b.setInvertColors(true); |
| }) |
| .with_bg(bg)); |
| } |
| } |
| |
| { |
| sk_sp<SkPathEffect> effect = SkDiscretePathEffect::Make(3, 5); |
| { |
| // Discrete path effects need a stroke width for drawPointsAsPoints |
| // to do something realistic |
| // And a Discrete(3, 5) effect produces miters that are near |
| // maximal for a miter limit of 3.0. |
| BoundsTolerance discrete_tolerance = |
| tolerance |
| // register the discrete offset so adjusters can compensate |
| .addDiscreteOffset(5) |
| // the miters in the 3-5 discrete effect don't always fill |
| // their conservative bounds, so tolerate a couple of pixels |
| .addBoundsPadding(2, 2); |
| RenderWith(testP, env, discrete_tolerance, |
| CaseParameters( |
| "PathEffect == Discrete-3-5", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStrokeWidth(5.0); |
| p.setStrokeMiter(3.0); |
| p.setPathEffect(effect); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStrokeWidth(5.0); |
| b.setStrokeMiter(3.0); |
| b.setPathEffect(DlPathEffect::From(effect).get()); |
| })); |
| } |
| EXPECT_TRUE(testP.is_draw_text_blob() || effect->unique()) |
| << "PathEffect == Discrete-3-5 Cleanup"; |
| effect = SkDiscretePathEffect::Make(2, 3); |
| { |
| // Discrete path effects need a stroke width for drawPointsAsPoints |
| // to do something realistic |
| // And a Discrete(2, 3) effect produces miters that are near |
| // maximal for a miter limit of 2.5. |
| BoundsTolerance discrete_tolerance = |
| tolerance |
| // register the discrete offset so adjusters can compensate |
| .addDiscreteOffset(3) |
| // the miters in the 3-5 discrete effect don't always fill |
| // their conservative bounds, so tolerate a couple of pixels |
| .addBoundsPadding(2, 2); |
| RenderWith(testP, env, discrete_tolerance, |
| CaseParameters( |
| "PathEffect == Discrete-2-3", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStrokeWidth(5.0); |
| p.setStrokeMiter(2.5); |
| p.setPathEffect(effect); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStrokeWidth(5.0); |
| b.setStrokeMiter(2.5); |
| b.setPathEffect(DlPathEffect::From(effect).get()); |
| })); |
| } |
| EXPECT_TRUE(testP.is_draw_text_blob() || effect->unique()) |
| << "PathEffect == Discrete-2-3 Cleanup"; |
| } |
| |
| { |
| const DlBlurMaskFilter filter(kNormal_SkBlurStyle, 5.0); |
| BoundsTolerance blur_5_tolerance = tolerance.addBoundsPadding(4, 4); |
| { |
| // Stroked primitives need some non-trivial stroke size to be blurred |
| RenderWith(testP, env, blur_5_tolerance, |
| CaseParameters( |
| "MaskFilter == Blur 5", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStrokeWidth(5.0); |
| p.setMaskFilter(filter.skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStrokeWidth(5.0); |
| b.setMaskFilter(&filter); |
| })); |
| } |
| } |
| |
| { |
| SkPoint end_points[] = { |
| SkPoint::Make(kRenderBounds.fLeft, kRenderBounds.fTop), |
| SkPoint::Make(kRenderBounds.fRight, kRenderBounds.fBottom), |
| }; |
| DlColor colors[] = { |
| DlColor::kGreen(), |
| DlColor::kYellow().withAlpha(0x7f), |
| DlColor::kBlue(), |
| }; |
| float stops[] = { |
| 0.0, |
| 0.5, |
| 1.0, |
| }; |
| std::shared_ptr<DlColorSource> source = DlColorSource::MakeLinear( |
| end_points[0], end_points[1], 3, colors, stops, DlTileMode::kMirror); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "LinearGradient GYB", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setShader(source->skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setColorSource(source.get()); |
| })); |
| } |
| } |
| } |
| |
| static void RenderWithStrokes(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& tolerance_in) { |
| // The test cases were generated with geometry that will try to fill |
| // out the various miter limits used for testing, but they can be off |
| // by a couple of pixels so we will relax bounds testing for strokes by |
| // a couple of pixels. |
| BoundsTolerance tolerance = tolerance_in.addBoundsPadding(2, 2); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Fill", |
| [=](SkCanvas*, SkPaint& p) { // |
| p.setStyle(SkPaint::kFill_Style); |
| }, |
| [=](DisplayListBuilder& b) { // |
| b.setStyle(DlDrawStyle::kFill); |
| })); |
| // Skia on HW produces a strong miter consistent with width=1.0 |
| // for any width less than a pixel, but the bounds computations of |
| // both DL and SkPicture do not account for this. We will get |
| // OOB pixel errors for the highly mitered drawPath geometry if |
| // we don't set stroke width to 1.0 for that test on HW. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=14046 |
| bool no_hairlines = |
| testP.is_draw_path() && |
| env.provider()->backend_type() != BackendType::kSoftware_Backend; |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Stroke + defaults", |
| [=](SkCanvas*, SkPaint& p) { // |
| if (no_hairlines) { |
| p.setStrokeWidth(1.0); |
| } |
| p.setStyle(SkPaint::kStroke_Style); |
| }, |
| [=](DisplayListBuilder& b) { // |
| if (no_hairlines) { |
| b.setStrokeWidth(1.0); |
| } |
| b.setStyle(DlDrawStyle::kStroke); |
| })); |
| |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Fill + unnecessary StrokeWidth 10", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kFill_Style); |
| p.setStrokeWidth(10.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kFill); |
| b.setStrokeWidth(10.0); |
| })); |
| |
| RenderEnvironment stroke_base_env = |
| RenderEnvironment::MakeN32(env.provider()); |
| CvSetup cv_stroke_setup = [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| }; |
| DlRenderer dl_stroke_setup = [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| }; |
| stroke_base_env.init_ref(cv_stroke_setup, testP.cv_renderer(), |
| dl_stroke_setup); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 10", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(10.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(10.0); |
| })); |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Square Cap", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| p.setStrokeCap(SkPaint::kSquare_Cap); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| b.setStrokeCap(DlStrokeCap::kSquare); |
| })); |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Round Cap", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| p.setStrokeCap(SkPaint::kRound_Cap); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| b.setStrokeCap(DlStrokeCap::kRound); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Bevel Join", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| p.setStrokeJoin(SkPaint::kBevel_Join); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| b.setStrokeJoin(DlStrokeJoin::kBevel); |
| })); |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Round Join", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| p.setStrokeJoin(SkPaint::kRound_Join); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| b.setStrokeJoin(DlStrokeJoin::kRound); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Miter 10", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| p.setStrokeMiter(10.0); |
| p.setStrokeJoin(SkPaint::kMiter_Join); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| b.setStrokeMiter(10.0); |
| b.setStrokeJoin(DlStrokeJoin::kMiter); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Miter 0", |
| [=](SkCanvas*, SkPaint& p) { |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(5.0); |
| p.setStrokeMiter(0.0); |
| p.setStrokeJoin(SkPaint::kMiter_Join); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.setStyle(DlDrawStyle::kStroke); |
| b.setStrokeWidth(5.0); |
| b.setStrokeMiter(0.0); |
| b.setStrokeJoin(DlStrokeJoin::kMiter); |
| })); |
| |
| { |
| const SkScalar test_dashes_1[] = {29.0, 2.0}; |
| const SkScalar test_dashes_2[] = {17.0, 1.5}; |
| auto effect = DlDashPathEffect::Make(test_dashes_1, 2, 0.0f); |
| { |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "PathEffect == Dash-29-2", |
| [=](SkCanvas*, SkPaint& p) { |
| // Need stroke style to see dashing properly |
| p.setStyle(SkPaint::kStroke_Style); |
| // Provide some non-trivial stroke size to get dashed |
| p.setStrokeWidth(5.0); |
| p.setPathEffect(effect->skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| // Need stroke style to see dashing properly |
| b.setStyle(DlDrawStyle::kStroke); |
| // Provide some non-trivial stroke size to get dashed |
| b.setStrokeWidth(5.0); |
| b.setPathEffect(effect.get()); |
| })); |
| } |
| effect = DlDashPathEffect::Make(test_dashes_2, 2, 0.0f); |
| { |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "PathEffect == Dash-17-1.5", |
| [=](SkCanvas*, SkPaint& p) { |
| // Need stroke style to see dashing properly |
| p.setStyle(SkPaint::kStroke_Style); |
| // Provide some non-trivial stroke size to get dashed |
| p.setStrokeWidth(5.0); |
| p.setPathEffect(effect->skia_object()); |
| }, |
| [=](DisplayListBuilder& b) { |
| // Need stroke style to see dashing properly |
| b.setStyle(DlDrawStyle::kStroke); |
| // Provide some non-trivial stroke size to get dashed |
| b.setStrokeWidth(5.0); |
| b.setPathEffect(effect.get()); |
| })); |
| } |
| } |
| } |
| |
| static void RenderWithTransforms(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& tolerance) { |
| // If the rendering method does not fill the corners of the original |
| // bounds, then the estimate under rotation or skewing will be off |
| // so we scale the padding by about 5% to compensate. |
| BoundsTolerance skewed_tolerance = tolerance.mulScale(1.05, 1.05); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Translate 5, 10", // |
| [=](SkCanvas* c, SkPaint&) { c->translate(5, 10); }, |
| [=](DisplayListBuilder& b) { b.translate(5, 10); })); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Scale +5%", // |
| [=](SkCanvas* c, SkPaint&) { c->scale(1.05, 1.05); }, |
| [=](DisplayListBuilder& b) { b.scale(1.05, 1.05); })); |
| RenderWith(testP, env, skewed_tolerance, |
| CaseParameters( |
| "Rotate 5 degrees", // |
| [=](SkCanvas* c, SkPaint&) { c->rotate(5); }, |
| [=](DisplayListBuilder& b) { b.rotate(5); })); |
| RenderWith(testP, env, skewed_tolerance, |
| CaseParameters( |
| "Skew 5%", // |
| [=](SkCanvas* c, SkPaint&) { c->skew(0.05, 0.05); }, |
| [=](DisplayListBuilder& b) { b.skew(0.05, 0.05); })); |
| { |
| // This rather odd transform can cause slight differences in |
| // computing in-bounds samples depending on which base rendering |
| // routine Skia uses. Making sure our matrix values are powers |
| // of 2 reduces, but does not eliminate, these slight differences |
| // in calculation when we are comparing rendering with an alpha |
| // to rendering opaque colors in the group opacity tests, for |
| // example. |
| SkScalar tweak = 1.0 / 16.0; |
| SkMatrix tx = SkMatrix::MakeAll(1.0 + tweak, tweak, 5, // |
| tweak, 1.0 + tweak, 10, // |
| 0, 0, 1); |
| RenderWith(testP, env, skewed_tolerance, |
| CaseParameters( |
| "Transform 2D Affine", |
| [=](SkCanvas* c, SkPaint&) { c->concat(tx); }, |
| [=](DisplayListBuilder& b) { |
| b.transform2DAffine(tx[0], tx[1], tx[2], // |
| tx[3], tx[4], tx[5]); |
| })); |
| } |
| { |
| SkM44 m44 = SkM44(1, 0, 0, kRenderCenterX, // |
| 0, 1, 0, kRenderCenterY, // |
| 0, 0, 1, 0, // |
| 0, 0, .001, 1); |
| m44.preConcat( |
| SkM44::Rotate({1, 0, 0}, math::kPi / 60)); // 3 degrees around X |
| m44.preConcat( |
| SkM44::Rotate({0, 1, 0}, math::kPi / 45)); // 4 degrees around Y |
| m44.preTranslate(-kRenderCenterX, -kRenderCenterY); |
| RenderWith( |
| testP, env, skewed_tolerance, |
| CaseParameters( |
| "Transform Full Perspective", |
| [=](SkCanvas* c, SkPaint&) { c->concat(m44); }, // |
| [=](DisplayListBuilder& b) { |
| b.transformFullPerspective( |
| m44.rc(0, 0), m44.rc(0, 1), m44.rc(0, 2), m44.rc(0, 3), |
| m44.rc(1, 0), m44.rc(1, 1), m44.rc(1, 2), m44.rc(1, 3), |
| m44.rc(2, 0), m44.rc(2, 1), m44.rc(2, 2), m44.rc(2, 3), |
| m44.rc(3, 0), m44.rc(3, 1), m44.rc(3, 2), m44.rc(3, 3)); |
| })); |
| } |
| } |
| |
| static void RenderWithClips(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& diff_tolerance) { |
| // We used to use an inset of 15.5 pixels here, but since Skia's rounding |
| // behavior at the center of pixels does not match between HW and SW, we |
| // ended up with some clips including different pixels between the two |
| // destinations and this interacted poorly with the carefully chosen |
| // geometry in some of the tests which was designed to have just the |
| // right features fully filling the clips based on the SW rounding. By |
| // moving to a 15.4 inset, the edge of the clip is never on the "rounding |
| // edge" of a pixel. |
| SkRect r_clip = kRenderBounds.makeInset(15.4, 15.4); |
| BoundsTolerance intersect_tolerance = diff_tolerance.clip(r_clip); |
| intersect_tolerance = intersect_tolerance.addPostClipPadding(1, 1); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "Hard ClipRect inset by 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipRect(r_clip, SkClipOp::kIntersect, false); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipRect(r_clip, ClipOp::kIntersect, false); |
| })); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "AntiAlias ClipRect inset by 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipRect(r_clip, SkClipOp::kIntersect, true); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipRect(r_clip, ClipOp::kIntersect, true); |
| })); |
| RenderWith(testP, env, diff_tolerance, |
| CaseParameters( |
| "Hard ClipRect Diff, inset by 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipRect(r_clip, SkClipOp::kDifference, false); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipRect(r_clip, ClipOp::kDifference, false); |
| }) |
| .with_diff_clip()); |
| // This test RR clip used to use very small radii, but due to |
| // optimizations in the HW rrect rasterization, this caused small |
| // bulges in the corners of the RRect which were interpreted as |
| // "clip overruns" by the clip OOB pixel testing code. Using less |
| // abusively small radii fixes the problem. |
| SkRRect rr_clip = SkRRect::MakeRectXY(r_clip, 9, 9); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "Hard ClipRRect with radius of 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipRRect(rr_clip, SkClipOp::kIntersect, false); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipRRect(rr_clip, ClipOp::kIntersect, false); |
| })); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "AntiAlias ClipRRect with radius of 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipRRect(rr_clip, SkClipOp::kIntersect, true); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipRRect(rr_clip, ClipOp::kIntersect, true); |
| })); |
| RenderWith(testP, env, diff_tolerance, |
| CaseParameters( |
| "Hard ClipRRect Diff, with radius of 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipRRect(rr_clip, SkClipOp::kDifference, false); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipRRect(rr_clip, ClipOp::kDifference, false); |
| }) |
| .with_diff_clip()); |
| SkPath path_clip = SkPath(); |
| path_clip.setFillType(SkPathFillType::kEvenOdd); |
| path_clip.addRect(r_clip); |
| path_clip.addCircle(kRenderCenterX, kRenderCenterY, 1.0); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "Hard ClipPath inset by 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipPath(path_clip, SkClipOp::kIntersect, false); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipPath(path_clip, ClipOp::kIntersect, false); |
| })); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "AntiAlias ClipPath inset by 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipPath(path_clip, SkClipOp::kIntersect, true); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipPath(path_clip, ClipOp::kIntersect, true); |
| })); |
| RenderWith(testP, env, diff_tolerance, |
| CaseParameters( |
| "Hard ClipPath Diff, inset by 15.4", |
| [=](SkCanvas* c, SkPaint&) { |
| c->clipPath(path_clip, SkClipOp::kDifference, false); |
| }, |
| [=](DisplayListBuilder& b) { |
| b.clipPath(path_clip, ClipOp::kDifference, false); |
| }) |
| .with_diff_clip()); |
| } |
| |
| static sk_sp<SkPicture> getSkPicture(const TestParameters& testP, |
| const CaseParameters& caseP) { |
| SkPictureRecorder recorder; |
| SkRTreeFactory rtree_factory; |
| SkCanvas* cv = recorder.beginRecording(kTestBounds, &rtree_factory); |
| caseP.render_to(cv, testP); |
| return recorder.finishRecordingAsPicture(); |
| } |
| |
| static void RenderWith(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& tolerance_in, |
| const CaseParameters& caseP) { |
| const std::string info = env.backend_name() + ": " + caseP.info(); |
| const DlColor bg = caseP.bg(); |
| |
| // sk_result is a direct rendering via SkCanvas to SkSurface |
| // DisplayList mechanisms are not involved in this operation |
| // SkPaint sk_paint; |
| CvJobRenderer sk_job(caseP.cv_setup(), // |
| testP.cv_renderer(), // |
| caseP.cv_restore()); |
| RenderJobInfo base_info = { |
| .bg = caseP.bg(), |
| }; |
| auto sk_result = env.getResult(base_info, sk_job); |
| const BoundsTolerance tolerance = |
| testP.adjust(tolerance_in, sk_job.setup_paint(), sk_result->matrix()); |
| const sk_sp<SkPicture> sk_picture = sk_job.MakePicture(); |
| SkRect sk_bounds = sk_picture->cullRect(); |
| ASSERT_EQ(sk_result->width(), kTestWidth) << info; |
| ASSERT_EQ(sk_result->height(), kTestHeight) << info; |
| checkPixels(sk_result.get(), sk_bounds, info + " (Skia reference)", bg); |
| |
| DisplayListBuilder dl_attr; |
| caseP.dl_setup()(dl_attr); |
| if (testP.should_match(env, dl_attr, sk_result->matrix(), |
| sk_result->clip_bounds(), caseP.has_diff_clip(), |
| caseP.has_mutating_save_layer())) { |
| quickCompareToReference(env.ref_result(), sk_result.get(), true, |
| info + " (attribute has no effect)"); |
| } else { |
| quickCompareToReference(env.ref_result(), sk_result.get(), false, |
| info + " (attribute affects rendering)"); |
| } |
| |
| sk_sp<DisplayList> display_list; |
| { |
| // This sequence plays the provided equivalently constructed |
| // DisplayList onto the SkCanvas of the surface |
| // DisplayList => direct rendering |
| DlRenderJob dl_job(caseP.dl_setup(), // |
| testP.dl_renderer(), // |
| caseP.dl_restore()); |
| auto dl_result = env.getResult(base_info, dl_job); |
| display_list = dl_job.display_list(); |
| SkRect dl_bounds = display_list->bounds(); |
| if (!sk_bounds.roundOut().contains(dl_bounds)) { |
| FML_LOG(ERROR) << "For " << info; |
| FML_LOG(ERROR) << "sk ref: " // |
| << sk_bounds.fLeft << ", " << sk_bounds.fTop << " => " |
| << sk_bounds.fRight << ", " << sk_bounds.fBottom; |
| FML_LOG(ERROR) << "dl: " // |
| << dl_bounds.fLeft << ", " << dl_bounds.fTop << " => " |
| << dl_bounds.fRight << ", " << dl_bounds.fBottom; |
| if (!dl_bounds.contains(sk_bounds)) { |
| FML_LOG(ERROR) << "DisplayList bounds are too small!"; |
| } |
| if (!sk_bounds.roundOut().contains(dl_bounds.roundOut())) { |
| FML_LOG(ERROR) << "###### DisplayList bounds larger than reference!"; |
| } |
| } |
| |
| // This EXPECT sometimes triggers, but when it triggers and I examine |
| // the ref_bounds, they are always unnecessarily large and since the |
| // pixel OOB tests in the compare method do not trigger, we will trust |
| // the DL bounds. |
| // EXPECT_TRUE(dl_bounds.contains(ref_bounds)) << info; |
| |
| // When we are drawing a DisplayList, the display_list built above |
| // will contain just a single drawDisplayList call plus the case |
| // attribute. The sk_picture will, however, contain a list of all |
| // of the embedded calls in the display list and so the op counts |
| // will not be equal between the two. |
| if (!testP.is_draw_display_list()) { |
| EXPECT_EQ(static_cast<int>(display_list->op_count()), |
| sk_picture->approximateOpCount()) |
| << info; |
| } |
| |
| compareToReference(dl_result.get(), sk_result.get(), |
| info + " (DisplayList built directly -> surface)", |
| &dl_bounds, &tolerance, bg, |
| caseP.fuzzy_compare_components()); |
| |
| if (display_list->can_apply_group_opacity()) { |
| checkGroupOpacity(env, display_list, dl_result.get(), |
| info + " with Group Opacity", bg); |
| } |
| } |
| |
| // This test cannot work if the rendering is using shadows until |
| // we can access the Skia ShadowRec via public headers. |
| if (!testP.is_draw_shadows()) { |
| // This sequence renders SkCanvas calls to a DisplayList and then |
| // plays them back on SkCanvas to SkSurface |
| // SkCanvas calls => DisplayList => rendering |
| DisplayListCanvasRecorder dl_recorder(kTestBounds); |
| sk_job.Render(&dl_recorder, base_info); |
| DlRenderJob cv_dl_job(dl_recorder.Build()); |
| auto cv_dl_result = env.getResult(base_info, cv_dl_job); |
| compareToReference(cv_dl_result.get(), sk_result.get(), |
| info + " (Skia calls -> DisplayList -> surface)", |
| nullptr, nullptr, bg, |
| caseP.fuzzy_compare_components()); |
| } |
| |
| { |
| // This sequence renders the SkCanvas calls to an SkPictureRecorder and |
| // renders the DisplayList calls to a DisplayListBuilder and then |
| // renders both back under a transform (scale(2x)) to see if their |
| // rendering is affected differently by a change of matrix between |
| // recording time and rendering time. |
| const int test_width_2 = kTestWidth * 2; |
| const int test_height_2 = kTestHeight * 2; |
| const SkScalar test_scale = 2.0; |
| |
| CvPictureRenderJob sk_job_x2(sk_picture); |
| RenderJobInfo info_2x = { |
| .width = test_width_2, |
| .height = test_height_2, |
| .bg = bg, |
| .scale = test_scale, |
| }; |
| auto ref_x2_result = env.getResult(info_2x, sk_job_x2); |
| ASSERT_EQ(ref_x2_result->width(), test_width_2) << info; |
| ASSERT_EQ(ref_x2_result->height(), test_height_2) << info; |
| |
| DlRenderJob dl_job_x2(display_list); |
| auto test_x2_result = env.getResult(info_2x, dl_job_x2); |
| compareToReference(test_x2_result.get(), ref_x2_result.get(), |
| info + " (Both rendered scaled 2x)", nullptr, nullptr, |
| bg, caseP.fuzzy_compare_components(), // |
| test_width_2, test_height_2, false); |
| } |
| } |
| |
| static bool fuzzyCompare(uint32_t pixel_a, uint32_t pixel_b, int fudge) { |
| for (int i = 0; i < 32; i += 8) { |
| int comp_a = (pixel_a >> i) & 0xff; |
| int comp_b = (pixel_b >> i) & 0xff; |
| if (std::abs(comp_a - comp_b) > fudge) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| static int groupOpacityFudgeFactor(const RenderEnvironment& env) { |
| if (env.format() == PixelFormat::k565_PixelFormat) { |
| return 9; |
| } |
| if (env.provider()->backend_type() == BackendType::kOpenGL_Backend) { |
| // OpenGL gets a little fuzzy at times. Still, "within 5" (aka +/-4) |
| // for byte samples is not bad, though the other backends give +/-1 |
| return 5; |
| } |
| return 2; |
| } |
| static void checkGroupOpacity(const RenderEnvironment& env, |
| const sk_sp<DisplayList>& display_list, |
| const RenderResult* ref_result, |
| const std::string& info, |
| DlColor bg) { |
| SkScalar opacity = 128.0 / 255.0; |
| |
| DlRenderJob opacity_job(display_list); |
| RenderJobInfo opacity_info = { |
| .bg = bg, |
| .opacity = opacity, |
| }; |
| auto group_opacity_result = env.getResult(opacity_info, opacity_job); |
| |
| ASSERT_EQ(group_opacity_result->width(), kTestWidth) << info; |
| ASSERT_EQ(group_opacity_result->height(), kTestHeight) << info; |
| |
| ASSERT_EQ(ref_result->width(), kTestWidth) << info; |
| ASSERT_EQ(ref_result->height(), kTestHeight) << info; |
| |
| int pixels_touched = 0; |
| int pixels_different = 0; |
| int max_diff = 0; |
| // We need to allow some slight differences per component due to the |
| // fact that rearranging discrete calculations can compound round off |
| // errors. Off-by-2 is enough for 8 bit components, but for the 565 |
| // tests we allow at least 9 which is the maximum distance between |
| // samples when converted to 8 bits. (You might think it would be a |
| // max step of 8 converting 5 bits to 8 bits, but it is really |
| // converting 31 steps to 255 steps with an average step size of |
| // 8.23 - 24 of the steps are by 8, but 7 of them are by 9.) |
| int fudge = groupOpacityFudgeFactor(env); |
| for (int y = 0; y < kTestHeight; y++) { |
| const uint32_t* ref_row = ref_result->addr32(0, y); |
| const uint32_t* test_row = group_opacity_result->addr32(0, y); |
| for (int x = 0; x < kTestWidth; x++) { |
| uint32_t ref_pixel = ref_row[x]; |
| uint32_t test_pixel = test_row[x]; |
| if (ref_pixel != bg.argb || test_pixel != bg.argb) { |
| pixels_touched++; |
| for (int i = 0; i < 32; i += 8) { |
| int ref_comp = (ref_pixel >> i) & 0xff; |
| int bg_comp = (bg.argb >> i) & 0xff; |
| SkScalar faded_comp = bg_comp + (ref_comp - bg_comp) * opacity; |
| int test_comp = (test_pixel >> i) & 0xff; |
| if (std::abs(faded_comp - test_comp) > fudge) { |
| int diff = std::abs(faded_comp - test_comp); |
| if (max_diff < diff) { |
| max_diff = diff; |
| } |
| pixels_different++; |
| break; |
| } |
| } |
| } |
| } |
| } |
| ASSERT_GT(pixels_touched, 20) << info; |
| if (pixels_different > 1) { |
| FML_LOG(ERROR) << "max diff == " << max_diff << " for " << info; |
| } |
| ASSERT_LE(pixels_different, 1) << info; |
| } |
| |
| static void checkPixels(const RenderResult* ref_result, |
| const SkRect ref_bounds, |
| const std::string& info, |
| const DlColor bg) { |
| uint32_t untouched = bg.premultipliedArgb(); |
| int pixels_touched = 0; |
| int pixels_oob = 0; |
| SkIRect i_bounds = ref_bounds.roundOut(); |
| for (int y = 0; y < kTestHeight; y++) { |
| const uint32_t* ref_row = ref_result->addr32(0, y); |
| for (int x = 0; x < kTestWidth; x++) { |
| if (ref_row[x] != untouched) { |
| pixels_touched++; |
| if (!i_bounds.contains(x, y)) { |
| pixels_oob++; |
| } |
| } |
| } |
| } |
| ASSERT_EQ(pixels_oob, 0) << info; |
| ASSERT_GT(pixels_touched, 0) << info; |
| } |
| |
| static void quickCompareToReference(const RenderResult* ref_result, |
| const RenderResult* test_result, |
| bool should_match, |
| const std::string& info) { |
| int w = test_result->width(); |
| int h = test_result->height(); |
| ASSERT_EQ(w, ref_result->width()) << info; |
| ASSERT_EQ(h, ref_result->height()) << info; |
| int pixels_different = 0; |
| for (int y = 0; y < h; y++) { |
| const uint32_t* ref_row = ref_result->addr32(0, y); |
| const uint32_t* test_row = test_result->addr32(0, y); |
| for (int x = 0; x < w; x++) { |
| if (ref_row[x] != test_row[x]) { |
| if (should_match) { |
| FML_LOG(ERROR) << std::hex << ref_row[x] << " != " << test_row[x]; |
| } |
| pixels_different++; |
| } |
| } |
| } |
| if (should_match) { |
| ASSERT_EQ(pixels_different, 0) << info; |
| } else { |
| ASSERT_NE(pixels_different, 0) << info; |
| } |
| } |
| |
| static void compareToReference(const RenderResult* test_result, |
| const RenderResult* ref_result, |
| const std::string& info, |
| SkRect* bounds, |
| const BoundsTolerance* tolerance, |
| const DlColor bg, |
| bool fuzzyCompares = false, |
| int width = kTestWidth, |
| int height = kTestHeight, |
| bool printMismatches = false) { |
| uint32_t untouched = bg.premultipliedArgb(); |
| ASSERT_EQ(test_result->width(), width) << info; |
| ASSERT_EQ(test_result->height(), height) << info; |
| SkIRect i_bounds = |
| bounds ? bounds->roundOut() : SkIRect::MakeWH(width, height); |
| |
| int pixels_different = 0; |
| int pixels_oob = 0; |
| int min_x = width; |
| int min_y = height; |
| int max_x = 0; |
| int max_y = 0; |
| for (int y = 0; y < height; y++) { |
| const uint32_t* ref_row = ref_result->addr32(0, y); |
| const uint32_t* test_row = test_result->addr32(0, y); |
| for (int x = 0; x < width; x++) { |
| if (bounds && test_row[x] != untouched) { |
| if (min_x > x) { |
| min_x = x; |
| } |
| if (min_y > y) { |
| min_y = y; |
| } |
| if (max_x <= x) { |
| max_x = x + 1; |
| } |
| if (max_y <= y) { |
| max_y = y + 1; |
| } |
| if (!i_bounds.contains(x, y)) { |
| pixels_oob++; |
| } |
| } |
| bool match = fuzzyCompares ? fuzzyCompare(test_row[x], ref_row[x], 1) |
| : test_row[x] == ref_row[x]; |
| if (!match) { |
| if (printMismatches) { |
| FML_LOG(ERROR) << "pix[" << x << ", " << y |
| << "] mismatch: " << std::hex << test_row[x] |
| << "(test) != (ref)" << ref_row[x] << std::dec; |
| } |
| pixels_different++; |
| } |
| } |
| } |
| if (pixels_oob > 0) { |
| FML_LOG(ERROR) << "pix bounds[" // |
| << min_x << ", " << min_y << " => " << max_x << ", " |
| << max_y << "]"; |
| FML_LOG(ERROR) << "dl_bounds[" // |
| << bounds->fLeft << ", " << bounds->fTop // |
| << " => " // |
| << bounds->fRight << ", " << bounds->fBottom // |
| << "]"; |
| } else if (bounds) { |
| showBoundsOverflow(info, i_bounds, tolerance, min_x, min_y, max_x, max_y); |
| } |
| ASSERT_EQ(pixels_oob, 0) << info; |
| ASSERT_EQ(pixels_different, 0) << info; |
| } |
| |
| static void showBoundsOverflow(const std::string& info, |
| SkIRect& bounds, |
| const BoundsTolerance* tolerance, |
| int pixLeft, |
| int pixTop, |
| int pixRight, |
| int pixBottom) { |
| int pad_left = std::max(0, pixLeft - bounds.fLeft); |
| int pad_top = std::max(0, pixTop - bounds.fTop); |
| int pad_right = std::max(0, bounds.fRight - pixRight); |
| int pad_bottom = std::max(0, bounds.fBottom - pixBottom); |
| SkIRect pix_bounds = |
| SkIRect::MakeLTRB(pixLeft, pixTop, pixRight, pixBottom); |
| SkISize pix_size = pix_bounds.size(); |
| int pix_width = pix_size.width(); |
| int pix_height = pix_size.height(); |
| int worst_pad_x = std::max(pad_left, pad_right); |
| int worst_pad_y = std::max(pad_top, pad_bottom); |
| if (tolerance->overflows(pix_bounds, worst_pad_x, worst_pad_y)) { |
| FML_LOG(ERROR) << "Computed bounds for " << info; |
| FML_LOG(ERROR) << "pix bounds[" // |
| << pixLeft << ", " << pixTop << " => " // |
| << pixRight << ", " << pixBottom // |
| << "]"; |
| FML_LOG(ERROR) << "dl_bounds[" // |
| << bounds.fLeft << ", " << bounds.fTop // |
| << " => " // |
| << bounds.fRight << ", " << bounds.fBottom // |
| << "]"; |
| FML_LOG(ERROR) << "Bounds overly conservative by up to " // |
| << worst_pad_x << ", " << worst_pad_y // |
| << " (" << (worst_pad_x * 100.0 / pix_width) // |
| << "%, " << (worst_pad_y * 100.0 / pix_height) << "%)"; |
| int pix_area = pix_size.area(); |
| int dl_area = bounds.width() * bounds.height(); |
| FML_LOG(ERROR) << "Total overflow area: " << (dl_area - pix_area) // |
| << " (+" << (dl_area * 100.0 / pix_area - 100.0) // |
| << "% larger)"; |
| FML_LOG(ERROR); |
| } |
| } |
| |
| static const sk_sp<SkImage> kTestImage; |
| static const sk_sp<SkImage> makeTestImage() { |
| sk_sp<SkSurface> surface = |
| SkSurface::MakeRasterN32Premul(kRenderWidth, kRenderHeight); |
| SkCanvas* canvas = surface->getCanvas(); |
| SkPaint p0, p1; |
| p0.setStyle(SkPaint::kFill_Style); |
| p0.setColor(SkColorSetARGB(0xff, 0x00, 0xfe, 0x00)); // off-green |
| p1.setStyle(SkPaint::kFill_Style); |
| p1.setColor(SK_ColorBLUE); |
| // Some pixels need some transparency for DstIn testing |
| p1.setAlpha(128); |
| int cbdim = 5; |
| for (int y = 0; y < kRenderHeight; y += cbdim) { |
| for (int x = 0; x < kRenderWidth; x += cbdim) { |
| SkPaint& cellp = ((x + y) & 1) == 0 ? p0 : p1; |
| canvas->drawRect(SkRect::MakeXYWH(x, y, cbdim, cbdim), cellp); |
| } |
| } |
| return surface->makeImageSnapshot(); |
| } |
| |
| static const DlImageColorSource kTestImageColorSource; |
| |
| static sk_sp<SkTextBlob> MakeTextBlob(const std::string& string, |
| SkScalar font_height) { |
| SkFont font(SkTypeface::MakeFromName("ahem", SkFontStyle::Normal()), |
| font_height); |
| return SkTextBlob::MakeFromText(string.c_str(), string.size(), font, |
| SkTextEncoding::kUTF8); |
| } |
| }; |
| |
| std::vector<std::unique_ptr<DlSurfaceProvider>> |
| CanvasCompareTester::kTestProviders; |
| |
| BoundsTolerance CanvasCompareTester::DefaultTolerance = |
| BoundsTolerance().addAbsolutePadding(1, 1); |
| |
| const sk_sp<SkImage> CanvasCompareTester::kTestImage = makeTestImage(); |
| const DlImageColorSource CanvasCompareTester::kTestImageColorSource( |
| DlImage::Make(kTestImage), |
| DlTileMode::kRepeat, |
| DlTileMode::kRepeat, |
| DlImageSampling::kLinear); |
| |
| // Eventually this bare bones testing::Test fixture will subsume the |
| // CanvasCompareTester and the TestParameters could then become just |
| // configuration calls made upon the fixture. |
| template <typename BaseT> |
| class DisplayListCanvasTestBase : public BaseT, protected DisplayListOpFlags { |
| public: |
| DisplayListCanvasTestBase() = default; |
| |
| static bool StartsWith(std::string str, std::string prefix) { |
| if (prefix.length() > str.length()) { |
| return false; |
| } |
| for (size_t i = 0; i < prefix.length(); i++) { |
| if (str[i] != prefix[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| static void AddProvider(BackendType type, const std::string& name) { |
| auto provider = DlSurfaceProvider::Create(type); |
| if (provider == nullptr) { |
| FML_LOG(ERROR) << "provider " << name << " not supported (ignoring)"; |
| return; |
| } |
| provider->InitializeSurface(kTestWidth, kTestHeight, |
| PixelFormat::kN32Premul_PixelFormat); |
| CanvasCompareTester::kTestProviders.push_back(std::move(provider)); |
| } |
| |
| static void SetUpTestSuite() { |
| bool do_software = true; |
| bool do_opengl = false; |
| bool do_metal = false; |
| std::vector<std::string> args = ::testing::internal::GetArgvs(); |
| for (auto p_arg = std::next(args.begin()); p_arg != args.end(); p_arg++) { |
| std::string arg = *p_arg; |
| bool enable = true; |
| if (StartsWith(arg, "--no")) { |
| enable = false; |
| arg = "-" + arg.substr(4); |
| } |
| if (arg == "--enable-software") { |
| do_software = enable; |
| } else if (arg == "--enable-opengl" || arg == "--enable-gl") { |
| do_opengl = enable; |
| } else if (arg == "--enable-metal") { |
| do_metal = enable; |
| } |
| } |
| if (do_software) { |
| AddProvider(BackendType::kSoftware_Backend, "Software"); |
| } |
| if (do_opengl) { |
| AddProvider(BackendType::kOpenGL_Backend, "OpenGL"); |
| } |
| if (do_metal) { |
| AddProvider(BackendType::kMetal_Backend, "Metal"); |
| } |
| std::string providers = ""; |
| auto begin = CanvasCompareTester::kTestProviders.cbegin(); |
| auto end = CanvasCompareTester::kTestProviders.cend(); |
| while (begin != end) { |
| providers += " " + (*begin++)->backend_name(); |
| } |
| FML_LOG(INFO) << "Running tests on [" << providers << " ]"; |
| } |
| |
| static void TearDownTestSuite() { |
| // Deleting these provider objects allows Metal to clean up its |
| // resources before the exit handler reports them as leaks. |
| CanvasCompareTester::kTestProviders.clear(); |
| } |
| |
| private: |
| FML_DISALLOW_COPY_AND_ASSIGN(DisplayListCanvasTestBase); |
| }; |
| using DisplayListCanvas = DisplayListCanvasTestBase<::testing::Test>; |
| |
| TEST_F(DisplayListCanvas, DrawPaint) { |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawPaint(paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPaint(); |
| }, |
| kDrawPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawOpaqueColor) { |
| // We use a non-opaque color to avoid obliterating any backdrop filter output |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| // DrawColor is not tested against attributes because it is supposed |
| // to ignore them. So, if the paint has an alpha, it is because we |
| // are doing a saveLayer+backdrop test and we need to not flood over |
| // the backdrop output with a solid color. So, we perform an alpha |
| // drawColor for that case only. |
| SkColor color = SkColorSetA(0xFFFF00FF, paint.getAlpha()); |
| canvas->drawColor(color); |
| }, |
| [=](DisplayListBuilder& builder) { |
| DlColor color = 0xFFFF00FF; |
| color = color.withAlpha(builder.getColor().getAlpha()); |
| builder.drawColor(color, DlBlendMode::kSrcOver); |
| }, |
| kDrawColorFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawAlphaColor) { |
| // We use a non-opaque color to avoid obliterating any backdrop filter output |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawColor(0x7FFF00FF); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawColor(0x7FFF00FF, DlBlendMode::kSrcOver); |
| }, |
| kDrawColorFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawDiagonalLines) { |
| SkPoint p1 = SkPoint::Make(kRenderLeft, kRenderTop); |
| SkPoint p2 = SkPoint::Make(kRenderRight, kRenderBottom); |
| SkPoint p3 = SkPoint::Make(kRenderLeft, kRenderBottom); |
| SkPoint p4 = SkPoint::Make(kRenderRight, kRenderTop); |
| // Adding some edge center to edge center diagonals to better fill |
| // out the RRect Clip so bounds checking sees less empty bounds space. |
| SkPoint p5 = SkPoint::Make(kRenderCenterX, kRenderTop); |
| SkPoint p6 = SkPoint::Make(kRenderRight, kRenderCenterY); |
| SkPoint p7 = SkPoint::Make(kRenderLeft, kRenderCenterY); |
| SkPoint p8 = SkPoint::Make(kRenderCenterX, kRenderBottom); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| // Skia requires kStroke style on horizontal and vertical |
| // lines to get the bounds correct. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 |
| SkPaint p = paint; |
| p.setStyle(SkPaint::kStroke_Style); |
| canvas->drawLine(p1, p2, p); |
| canvas->drawLine(p3, p4, p); |
| canvas->drawLine(p5, p6, p); |
| canvas->drawLine(p7, p8, p); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawLine(p1, p2); |
| builder.drawLine(p3, p4); |
| builder.drawLine(p5, p6); |
| builder.drawLine(p7, p8); |
| }, |
| kDrawLineFlags) |
| .set_draw_line()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawHorizontalLine) { |
| SkPoint p1 = SkPoint::Make(kRenderLeft, kRenderCenterY); |
| SkPoint p2 = SkPoint::Make(kRenderRight, kRenderCenterY); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| // Skia requires kStroke style on horizontal and vertical |
| // lines to get the bounds correct. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 |
| SkPaint p = paint; |
| p.setStyle(SkPaint::kStroke_Style); |
| canvas->drawLine(p1, p2, p); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawLine(p1, p2); |
| }, |
| kDrawHVLineFlags) |
| .set_draw_line() |
| .set_horizontal_line()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawVerticalLine) { |
| SkPoint p1 = SkPoint::Make(kRenderCenterX, kRenderTop); |
| SkPoint p2 = SkPoint::Make(kRenderCenterY, kRenderBottom); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| // Skia requires kStroke style on horizontal and vertical |
| // lines to get the bounds correct. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 |
| SkPaint p = paint; |
| p.setStyle(SkPaint::kStroke_Style); |
| canvas->drawLine(p1, p2, p); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawLine(p1, p2); |
| }, |
| kDrawHVLineFlags) |
| .set_draw_line() |
| .set_vertical_line()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawRect) { |
| // Bounds are offset by 0.5 pixels to induce AA |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawRect(kRenderBounds.makeOffset(0.5, 0.5), paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawRect(kRenderBounds.makeOffset(0.5, 0.5)); |
| }, |
| kDrawRectFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawOval) { |
| SkRect rect = kRenderBounds.makeInset(0, 10); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawOval(rect, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawOval(rect); |
| }, |
| kDrawOvalFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawCircle) { |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawCircle(kTestCenter, kRenderRadius, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawCircle(kTestCenter, kRenderRadius); |
| }, |
| kDrawCircleFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawRRect) { |
| SkRRect rrect = SkRRect::MakeRectXY(kRenderBounds, kRenderCornerRadius, |
| kRenderCornerRadius); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawRRect(rrect, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawRRect(rrect); |
| }, |
| kDrawRRectFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawDRRect) { |
| SkRRect outer = SkRRect::MakeRectXY(kRenderBounds, kRenderCornerRadius, |
| kRenderCornerRadius); |
| SkRect inner_bounds = kRenderBounds.makeInset(30.0, 30.0); |
| SkRRect inner = SkRRect::MakeRectXY(inner_bounds, kRenderCornerRadius, |
| kRenderCornerRadius); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawDRRect(outer, inner, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawDRRect(outer, inner); |
| }, |
| kDrawDRRectFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPath) { |
| SkPath path; |
| |
| // unclosed lines to show some caps |
| path.moveTo(kRenderLeft + 15, kRenderTop + 15); |
| path.lineTo(kRenderRight - 15, kRenderBottom - 15); |
| path.moveTo(kRenderLeft + 15, kRenderBottom - 15); |
| path.lineTo(kRenderRight - 15, kRenderTop + 15); |
| |
| path.addRect(kRenderBounds); |
| |
| // miter diamonds horizontally and vertically to show miters |
| path.moveTo(kVerticalMiterDiamondPoints[0]); |
| for (int i = 1; i < kVerticalMiterDiamondPointCount; i++) { |
| path.lineTo(kVerticalMiterDiamondPoints[i]); |
| } |
| path.close(); |
| path.moveTo(kHorizontalMiterDiamondPoints[0]); |
| for (int i = 1; i < kHorizontalMiterDiamondPointCount; i++) { |
| path.lineTo(kHorizontalMiterDiamondPoints[i]); |
| } |
| path.close(); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawPath(path, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPath(path); |
| }, |
| kDrawPathFlags) |
| .set_draw_path()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawArc) { |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawArc(kRenderBounds, 60, 330, false, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawArc(kRenderBounds, 60, 330, false); |
| }, |
| kDrawArcNoCenterFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawArcCenter) { |
| // Center arcs that inscribe nearly a whole circle except for a small |
| // arc extent gap have 2 angles that may appear or disappear at the |
| // various miter limits tested (0, 4, and 10). |
| // The center angle here is 12 degrees which shows a miter |
| // at limit=10, but not 0 or 4. |
| // The arcs at the corners where it turns in towards the |
| // center show miters at 4 and 10, but not 0. |
| // Limit == 0, neither corner does a miter |
| // Limit == 4, only the edge "turn-in" corners miter |
| // Limit == 10, edge and center corners all miter |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawArc(kRenderBounds, 60, 360 - 12, true, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawArc(kRenderBounds, 60, 360 - 12, true); |
| }, |
| kDrawArcWithCenterFlags) |
| .set_draw_arc_center()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPointsAsPoints) { |
| // The +/- 16 points are designed to fall just inside the clips |
| // that are tested against so we avoid lots of undrawn pixels |
| // in the accumulated bounds. |
| const SkScalar x0 = kRenderLeft; |
| const SkScalar x1 = kRenderLeft + 16; |
| const SkScalar x2 = (kRenderLeft + kRenderCenterX) * 0.5; |
| const SkScalar x3 = kRenderCenterX + 0.1; |
| const SkScalar x4 = (kRenderRight + kRenderCenterX) * 0.5; |
| const SkScalar x5 = kRenderRight - 16; |
| const SkScalar x6 = kRenderRight; |
| |
| const SkScalar y0 = kRenderTop; |
| const SkScalar y1 = kRenderTop + 16; |
| const SkScalar y2 = (kRenderTop + kRenderCenterY) * 0.5; |
| const SkScalar y3 = kRenderCenterY + 0.1; |
| const SkScalar y4 = (kRenderBottom + kRenderCenterY) * 0.5; |
| const SkScalar y5 = kRenderBottom - 16; |
| const SkScalar y6 = kRenderBottom; |
| |
| // clang-format off |
| const SkPoint points[] = { |
| {x0, y0}, {x1, y0}, {x2, y0}, {x3, y0}, {x4, y0}, {x5, y0}, {x6, y0}, |
| {x0, y1}, {x1, y1}, {x2, y1}, {x3, y1}, {x4, y1}, {x5, y1}, {x6, y1}, |
| {x0, y2}, {x1, y2}, {x2, y2}, {x3, y2}, {x4, y2}, {x5, y2}, {x6, y2}, |
| {x0, y3}, {x1, y3}, {x2, y3}, {x3, y3}, {x4, y3}, {x5, y3}, {x6, y3}, |
| {x0, y4}, {x1, y4}, {x2, y4}, {x3, y4}, {x4, y4}, {x5, y4}, {x6, y4}, |
| {x0, y5}, {x1, y5}, {x2, y5}, {x3, y5}, {x4, y5}, {x5, y5}, {x6, y5}, |
| {x0, y6}, {x1, y6}, {x2, y6}, {x3, y6}, {x4, y6}, {x5, y6}, {x6, y6}, |
| }; |
| // clang-format on |
| const int count = sizeof(points) / sizeof(points[0]); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| // Skia requires kStroke style on horizontal and vertical |
| // lines to get the bounds correct. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 |
| SkPaint p = paint; |
| p.setStyle(SkPaint::kStroke_Style); |
| canvas->drawPoints(SkCanvas::kPoints_PointMode, count, points, p); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPoints(PointMode::kPoints, count, points); |
| }, |
| kDrawPointsAsPointsFlags) |
| .set_draw_line() |
| .set_ignores_dashes()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPointsAsLines) { |
| const SkScalar x0 = kRenderLeft + 1; |
| const SkScalar x1 = kRenderLeft + 16; |
| const SkScalar x2 = kRenderRight - 16; |
| const SkScalar x3 = kRenderRight - 1; |
| |
| const SkScalar y0 = kRenderTop; |
| const SkScalar y1 = kRenderTop + 16; |
| const SkScalar y2 = kRenderBottom - 16; |
| const SkScalar y3 = kRenderBottom; |
| |
| // clang-format off |
| const SkPoint points[] = { |
| // Outer box |
| {x0, y0}, {x3, y0}, |
| {x3, y0}, {x3, y3}, |
| {x3, y3}, {x0, y3}, |
| {x0, y3}, {x0, y0}, |
| |
| // Diagonals |
| {x0, y0}, {x3, y3}, {x3, y0}, {x0, y3}, |
| |
| // Inner box |
| {x1, y1}, {x2, y1}, |
| {x2, y1}, {x2, y2}, |
| {x2, y2}, {x1, y2}, |
| {x1, y2}, {x1, y1}, |
| }; |
| // clang-format on |
| |
| const int count = sizeof(points) / sizeof(points[0]); |
| ASSERT_TRUE((count & 1) == 0); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| // Skia requires kStroke style on horizontal and vertical |
| // lines to get the bounds correct. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 |
| SkPaint p = paint; |
| p.setStyle(SkPaint::kStroke_Style); |
| canvas->drawPoints(SkCanvas::kLines_PointMode, count, points, p); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPoints(PointMode::kLines, count, points); |
| }, |
| kDrawPointsAsLinesFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPointsAsPolygon) { |
| const SkPoint points1[] = { |
| // RenderBounds box with a diamond |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderBottom), |
| SkPoint::Make(kRenderLeft, kRenderBottom), |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| SkPoint::Make(kRenderCenterX, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderCenterY), |
| SkPoint::Make(kRenderCenterX, kRenderBottom), |
| SkPoint::Make(kRenderLeft, kRenderCenterY), |
| }; |
| const int count1 = sizeof(points1) / sizeof(points1[0]); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| // Skia requires kStroke style on horizontal and vertical |
| // lines to get the bounds correct. |
| // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 |
| SkPaint p = paint; |
| p.setStyle(SkPaint::kStroke_Style); |
| canvas->drawPoints(SkCanvas::kPolygon_PointMode, count1, points1, |
| p); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPoints(PointMode::kPolygon, count1, points1); |
| }, |
| kDrawPointsAsPolygonFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawVerticesWithColors) { |
| // Cover as many sides of the box with only 6 vertices: |
| // +----------+ |
| // |xxxxxxxxxx| |
| // | xxxxxx| |
| // | xxx| |
| // |xxx | |
| // |xxxxxx | |
| // |xxxxxxxxxx| |
| // +----------| |
| const SkPoint pts[6] = { |
| // Upper-Right corner, full top, half right coverage |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderCenterY), |
| // Lower-Left corner, full bottom, half left coverage |
| SkPoint::Make(kRenderLeft, kRenderBottom), |
| SkPoint::Make(kRenderLeft, kRenderCenterY), |
| SkPoint::Make(kRenderRight, kRenderBottom), |
| }; |
| const DlColor colors[6] = { |
| SK_ColorRED, SK_ColorBLUE, SK_ColorGREEN, |
| SK_ColorCYAN, SK_ColorYELLOW, SK_ColorMAGENTA, |
| }; |
| const std::shared_ptr<DlVertices> vertices = |
| DlVertices::Make(DlVertexMode::kTriangles, 6, pts, nullptr, colors); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawVertices(vertices->skia_object(), SkBlendMode::kSrcOver, |
| paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawVertices(vertices, DlBlendMode::kSrcOver); |
| }, |
| kDrawVerticesFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawVerticesWithImage) { |
| // Cover as many sides of the box with only 6 vertices: |
| // +----------+ |
| // |xxxxxxxxxx| |
| // | xxxxxx| |
| // | xxx| |
| // |xxx | |
| // |xxxxxx | |
| // |xxxxxxxxxx| |
| // +----------| |
| const SkPoint pts[6] = { |
| // Upper-Right corner, full top, half right coverage |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderTop), |
| SkPoint::Make(kRenderRight, kRenderCenterY), |
| // Lower-Left corner, full bottom, half left coverage |
| SkPoint::Make(kRenderLeft, kRenderBottom), |
| SkPoint::Make(kRenderLeft, kRenderCenterY), |
| SkPoint::Make(kRenderRight, kRenderBottom), |
| }; |
| const SkPoint tex[6] = { |
| SkPoint::Make(kRenderWidth / 2.0, 0), |
| SkPoint::Make(0, kRenderHeight), |
| SkPoint::Make(kRenderWidth, kRenderHeight), |
| SkPoint::Make(kRenderWidth / 2, kRenderHeight), |
| SkPoint::Make(0, 0), |
| SkPoint::Make(kRenderWidth, 0), |
| }; |
| const std::shared_ptr<DlVertices> vertices = |
| DlVertices::Make(DlVertexMode::kTriangles, 6, pts, tex, nullptr); |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| SkPaint v_paint = paint; |
| if (v_paint.getShader() == nullptr) { |
| v_paint.setShader( |
| CanvasCompareTester::kTestImageColorSource.skia_object()); |
| } |
| canvas->drawVertices(vertices->skia_object(), SkBlendMode::kSrcOver, |
| v_paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| if (builder.getColorSource() == nullptr) { |
| builder.setColorSource( |
| &CanvasCompareTester::kTestImageColorSource); |
| } |
| builder.drawVertices(vertices, DlBlendMode::kSrcOver); |
| }, |
| kDrawVerticesFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageNearest) { |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawImage(CanvasCompareTester::kTestImage, // |
| kRenderLeft, kRenderTop, |
| ToSk(DlImageSampling::kNearestNeighbor), &paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawImage(DlImage::Make(CanvasCompareTester::kTestImage), |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| DlImageSampling::kNearestNeighbor, true); |
| }, |
| kDrawImageWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageNearestNoPaint) { |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawImage(CanvasCompareTester::kTestImage, // |
| kRenderLeft, kRenderTop, |
| ToSk(DlImageSampling::kNearestNeighbor), nullptr); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawImage(DlImage::Make(CanvasCompareTester::kTestImage), |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| DlImageSampling::kNearestNeighbor, false); |
| }, |
| kDrawImageFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageLinear) { |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawImage(CanvasCompareTester::kTestImage, // |
| kRenderLeft, kRenderTop, |
| ToSk(DlImageSampling::kLinear), &paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawImage(DlImage::Make(CanvasCompareTester::kTestImage), |
| SkPoint::Make(kRenderLeft, kRenderTop), |
| DlImageSampling::kLinear, true); |
| }, |
| kDrawImageWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageRectNearest) { |
| SkRect src = SkRect::MakeIWH(kRenderWidth, kRenderHeight).makeInset(5, 5); |
| SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawImageRect(CanvasCompareTester::kTestImage, src, dst, |
| ToSk(DlImageSampling::kNearestNeighbor), |
| &paint, SkCanvas::kFast_SrcRectConstraint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawImageRect( |
| DlImage::Make(CanvasCompareTester::kTestImage), src, dst, |
| DlImageSampling::kNearestNeighbor, true); |
| }, |
| kDrawImageRectWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageRectNearestNoPaint) { |
| SkRect src = SkRect::MakeIWH(kRenderWidth, kRenderHeight).makeInset(5, 5); |
| SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawImageRect(CanvasCompareTester::kTestImage, src, dst, |
| ToSk(DlImageSampling::kNearestNeighbor), |
| nullptr, SkCanvas::kFast_SrcRectConstraint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawImageRect( |
| DlImage::Make(CanvasCompareTester::kTestImage), src, dst, |
| DlImageSampling::kNearestNeighbor, false); |
| }, |
| kDrawImageRectFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageRectLinear) { |
| SkRect src = SkRect::MakeIWH(kRenderWidth, kRenderHeight).makeInset(5, 5); |
| SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawImageRect(CanvasCompareTester::kTestImage, src, dst, |
| ToSk(DlImageSampling::kLinear), &paint, |
| SkCanvas::kFast_SrcRectConstraint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawImageRect( |
| DlImage::Make(CanvasCompareTester::kTestImage), src, dst, |
| DlImageSampling::kLinear, true); |
| }, |
| kDrawImageRectWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageNineNearest) { |
| SkIRect src = SkIRect::MakeWH(kRenderWidth, kRenderHeight).makeInset(25, 25); |
| SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawImageNine(image.get(), src, dst, SkFilterMode::kNearest, |
| &paint); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawImageNine(DlImage::Make(image), src, dst, |
| DlFilterMode::kNearest, true); |
| }, |
| kDrawImageNineWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageNineNearestNoPaint) { |
| SkIRect src = SkIRect::MakeWH(kRenderWidth, kRenderHeight).makeInset(25, 25); |
| SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawImageNine(image.get(), src, dst, SkFilterMode::kNearest, |
| nullptr); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawImageNine(DlImage::Make(image), src, dst, |
| DlFilterMode::kNearest, false); |
| }, |
| kDrawImageNineFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageNineLinear) { |
| SkIRect src = SkIRect::MakeWH(kRenderWidth, kRenderHeight).makeInset(25, 25); |
| SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawImageNine(image.get(), src, dst, SkFilterMode::kLinear, |
| &paint); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawImageNine(DlImage::Make(image), src, dst, |
| DlFilterMode::kLinear, true); |
| }, |
| kDrawImageNineWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageLatticeNearest) { |
| const SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| const int div_x[] = { |
| kRenderWidth * 1 / 4, |
| kRenderWidth * 2 / 4, |
| kRenderWidth * 3 / 4, |
| }; |
| const int div_y[] = { |
| kRenderHeight * 1 / 4, |
| kRenderHeight * 2 / 4, |
| kRenderHeight * 3 / 4, |
| }; |
| SkCanvas::Lattice lattice = { |
| div_x, div_y, nullptr, 3, 3, nullptr, nullptr, |
| }; |
| sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawImageLattice(image.get(), lattice, dst, |
| SkFilterMode::kNearest, &paint); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawImageLattice(DlImage::Make(image), lattice, dst, |
| DlFilterMode::kNearest, true); |
| }, |
| kDrawImageLatticeWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageLatticeNearestNoPaint) { |
| const SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| const int div_x[] = { |
| kRenderWidth * 1 / 4, |
| kRenderWidth * 2 / 4, |
| kRenderWidth * 3 / 4, |
| }; |
| const int div_y[] = { |
| kRenderHeight * 1 / 4, |
| kRenderHeight * 2 / 4, |
| kRenderHeight * 3 / 4, |
| }; |
| SkCanvas::Lattice lattice = { |
| div_x, div_y, nullptr, 3, 3, nullptr, nullptr, |
| }; |
| sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawImageLattice(image.get(), lattice, dst, |
| SkFilterMode::kNearest, nullptr); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawImageLattice(DlImage::Make(image), lattice, dst, |
| DlFilterMode::kNearest, false); |
| }, |
| kDrawImageLatticeFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawImageLatticeLinear) { |
| const SkRect dst = kRenderBounds.makeInset(10.5, 10.5); |
| const int div_x[] = { |
| kRenderWidth / 4, |
| kRenderWidth / 2, |
| kRenderWidth * 3 / 4, |
| }; |
| const int div_y[] = { |
| kRenderHeight / 4, |
| kRenderHeight / 2, |
| kRenderHeight * 3 / 4, |
| }; |
| SkCanvas::Lattice lattice = { |
| div_x, div_y, nullptr, 3, 3, nullptr, nullptr, |
| }; |
| sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| canvas->drawImageLattice(image.get(), lattice, dst, |
| SkFilterMode::kLinear, &paint); |
| }, |
| [=](DisplayListBuilder& builder) { |
| builder.drawImageLattice(DlImage::Make(image), lattice, dst, |
| DlFilterMode::kLinear, true); |
| }, |
| kDrawImageLatticeWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawAtlasNearest) { |
| const SkRSXform xform[] = { |
| // clang-format off |
| { 1.2f, 0.0f, kRenderLeft, kRenderTop}, |
| { 0.0f, 1.2f, kRenderRight, kRenderTop}, |
| {-1.2f, 0.0f, kRenderRight, kRenderBottom}, |
| { 0.0f, -1.2f, kRenderLeft, kRenderBottom}, |
| // clang-format on |
| }; |
| const SkRect tex[] = { |
| // clang-format off |
| {0, 0, kRenderHalfWidth, kRenderHalfHeight}, |
| {kRenderHalfWidth, 0, kRenderWidth, kRenderHalfHeight}, |
| {kRenderHalfWidth, kRenderHalfHeight, kRenderWidth, kRenderHeight}, |
| {0, kRenderHalfHeight, kRenderHalfWidth, kRenderHeight}, |
| // clang-format on |
| }; |
| const DlColor colors[] = { |
| SK_ColorBLUE, |
| SK_ColorGREEN, |
| SK_ColorYELLOW, |
| SK_ColorMAGENTA, |
| }; |
| const sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| const DlImageSampling sampling = DlImageSampling::kNearestNeighbor; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| const SkColor* sk_colors = reinterpret_cast<const SkColor*>(colors); |
| canvas->drawAtlas(image.get(), xform, tex, sk_colors, 4, |
| SkBlendMode::kSrcOver, ToSk(sampling), nullptr, |
| &paint); |
| }, |
| [=](DisplayListBuilder& builder) { |
| const DlColor* dl_colors = reinterpret_cast<const DlColor*>(colors); |
| builder.drawAtlas(DlImage::Make(image), xform, tex, dl_colors, 4, |
| DlBlendMode::kSrcOver, sampling, nullptr, true); |
| }, |
| kDrawAtlasWithPaintFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawAtlasNearestNoPaint) { |
| const SkRSXform xform[] = { |
| // clang-format off |
| { 1.2f, 0.0f, kRenderLeft, kRenderTop}, |
| { 0.0f, 1.2f, kRenderRight, kRenderTop}, |
| {-1.2f, 0.0f, kRenderRight, kRenderBottom}, |
| { 0.0f, -1.2f, kRenderLeft, kRenderBottom}, |
| // clang-format on |
| }; |
| const SkRect tex[] = { |
| // clang-format off |
| {0, 0, kRenderHalfWidth, kRenderHalfHeight}, |
| {kRenderHalfWidth, 0, kRenderWidth, kRenderHalfHeight}, |
| {kRenderHalfWidth, kRenderHalfHeight, kRenderWidth, kRenderHeight}, |
| {0, kRenderHalfHeight, kRenderHalfWidth, kRenderHeight}, |
| // clang-format on |
| }; |
| const DlColor colors[] = { |
| SK_ColorBLUE, |
| SK_ColorGREEN, |
| SK_ColorYELLOW, |
| SK_ColorMAGENTA, |
| }; |
| const sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| const DlImageSampling sampling = DlImageSampling::kNearestNeighbor; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| const SkColor* sk_colors = reinterpret_cast<const SkColor*>(colors); |
| canvas->drawAtlas(image.get(), xform, tex, sk_colors, 4, |
| SkBlendMode::kSrcOver, ToSk(sampling), // |
| nullptr, nullptr); |
| }, |
| [=](DisplayListBuilder& builder) { |
| const DlColor* dl_colors = reinterpret_cast<const DlColor*>(colors); |
| builder.drawAtlas(DlImage::Make(image), xform, tex, dl_colors, 4, |
| DlBlendMode::kSrcOver, sampling, // |
| nullptr, false); |
| }, |
| kDrawAtlasFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawAtlasLinear) { |
| const SkRSXform xform[] = { |
| // clang-format off |
| { 1.2f, 0.0f, kRenderLeft, kRenderTop}, |
| { 0.0f, 1.2f, kRenderRight, kRenderTop}, |
| {-1.2f, 0.0f, kRenderRight, kRenderBottom}, |
| { 0.0f, -1.2f, kRenderLeft, kRenderBottom}, |
| // clang-format on |
| }; |
| const SkRect tex[] = { |
| // clang-format off |
| {0, 0, kRenderHalfWidth, kRenderHalfHeight}, |
| {kRenderHalfWidth, 0, kRenderWidth, kRenderHalfHeight}, |
| {kRenderHalfWidth, kRenderHalfHeight, kRenderWidth, kRenderHeight}, |
| {0, kRenderHalfHeight, kRenderHalfWidth, kRenderHeight}, |
| // clang-format on |
| }; |
| const DlColor colors[] = { |
| SK_ColorBLUE, |
| SK_ColorGREEN, |
| SK_ColorYELLOW, |
| SK_ColorMAGENTA, |
| }; |
| const sk_sp<SkImage> image = CanvasCompareTester::kTestImage; |
| const DlImageSampling sampling = DlImageSampling::kLinear; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { |
| const SkColor* sk_colors = reinterpret_cast<const SkColor*>(colors); |
| canvas->drawAtlas(image.get(), xform, tex, sk_colors, 2, // |
| SkBlendMode::kSrcOver, ToSk(sampling), nullptr, |
| &paint); |
| }, |
| [=](DisplayListBuilder& builder) { |
| const DlColor* dl_colors = reinterpret_cast<const DlColor*>(colors); |
| builder.drawAtlas(DlImage::Make(image), xform, tex, dl_colors, 2, |
| DlBlendMode::kSrcOver, sampling, nullptr, true); |
| }, |
| kDrawAtlasWithPaintFlags)); |
| } |
| |
| sk_sp<SkPicture> makeTestPicture() { |
| SkPictureRecorder recorder; |
| SkRTreeFactory rtree_factory; |
| SkCanvas* cv = recorder.beginRecording(kRenderBounds, &rtree_factory); |
| SkPaint p; |
| p.setStyle(SkPaint::kFill_Style); |
| p.setColor(SK_ColorRED); |
| cv->drawRect({kRenderLeft, kRenderTop, kRenderCenterX, kRenderCenterY}, p); |
| p.setColor(SK_ColorBLUE); |
| cv->drawRect({kRenderCenterX, kRenderTop, kRenderRight, kRenderCenterY}, p); |
| p.setColor(SK_ColorGREEN); |
| cv->drawRect({kRenderLeft, kRenderCenterY, kRenderCenterX, kRenderBottom}, p); |
| p.setColor(SK_ColorYELLOW); |
| cv->drawRect({kRenderCenterX, kRenderCenterY, kRenderRight, kRenderBottom}, |
| p); |
| return recorder.finishRecordingAsPicture(); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPicture) { |
| sk_sp<SkPicture> picture = makeTestPicture(); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawPicture(picture, nullptr, nullptr); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPicture(picture, nullptr, false); |
| }, |
| kDrawPictureFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPictureWithMatrix) { |
| sk_sp<SkPicture> picture = makeTestPicture(); |
| SkMatrix matrix = SkMatrix::Scale(0.9, 0.9); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawPicture(picture, &matrix, nullptr); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPicture(picture, &matrix, false); |
| }, |
| kDrawPictureFlags)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawPictureWithPaint) { |
| sk_sp<SkPicture> picture = makeTestPicture(); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawPicture(picture, nullptr, &paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawPicture(picture, nullptr, true); |
| }, |
| kDrawPictureWithPaintFlags)); |
| } |
| |
| sk_sp<DisplayList> makeTestDisplayList() { |
| DisplayListBuilder builder; |
| builder.setStyle(DlDrawStyle::kFill); |
| builder.setColor(SK_ColorRED); |
| builder.drawRect({kRenderLeft, kRenderTop, kRenderCenterX, kRenderCenterY}); |
| builder.setColor(SK_ColorBLUE); |
| builder.drawRect({kRenderCenterX, kRenderTop, kRenderRight, kRenderCenterY}); |
| builder.setColor(SK_ColorGREEN); |
| builder.drawRect( |
| {kRenderLeft, kRenderCenterY, kRenderCenterX, kRenderBottom}); |
| builder.setColor(SK_ColorYELLOW); |
| builder.drawRect( |
| {kRenderCenterX, kRenderCenterY, kRenderRight, kRenderBottom}); |
| return builder.Build(); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawDisplayList) { |
| sk_sp<DisplayList> display_list = makeTestDisplayList(); |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| display_list->RenderTo(canvas); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawDisplayList(display_list); |
| }, |
| kDrawDisplayListFlags) |
| .set_draw_display_list()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawTextBlob) { |
| // TODO(https://github.com/flutter/flutter/issues/82202): Remove once the |
| // performance overlay can use Fuchsia's font manager instead of the empty |
| // default. |
| #if defined(OS_FUCHSIA) |
| GTEST_SKIP() << "Rendering comparisons require a valid default font manager"; |
| #endif // OS_FUCHSIA |
| sk_sp<SkTextBlob> blob = |
| CanvasCompareTester::MakeTextBlob("Testing", kRenderHeight * 0.33f); |
| SkScalar render_y_1_3 = kRenderTop + kRenderHeight * 0.3; |
| SkScalar render_y_2_3 = kRenderTop + kRenderHeight * 0.6; |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| canvas->drawTextBlob(blob, kRenderLeft, render_y_1_3, paint); |
| canvas->drawTextBlob(blob, kRenderLeft, render_y_2_3, paint); |
| canvas->drawTextBlob(blob, kRenderLeft, kRenderBottom, paint); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawTextBlob(blob, kRenderLeft, render_y_1_3); |
| builder.drawTextBlob(blob, kRenderLeft, render_y_2_3); |
| builder.drawTextBlob(blob, kRenderLeft, kRenderBottom); |
| }, |
| kDrawTextBlobFlags) |
| .set_draw_text_blob(), |
| // From examining the bounds differential for the "Default" case, the |
| // SkTextBlob adds a padding of ~32 on the left, ~30 on the right, |
| // ~12 on top and ~8 on the bottom, so we add 33h & 13v allowed |
| // padding to the tolerance |
| CanvasCompareTester::DefaultTolerance.addBoundsPadding(33, 13)); |
| EXPECT_TRUE(blob->unique()); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawShadow) { |
| SkPath path; |
| path.addRoundRect( |
| { |
| kRenderLeft + 10, |
| kRenderTop, |
| kRenderRight - 10, |
| kRenderBottom - 20, |
| }, |
| kRenderCornerRadius, kRenderCornerRadius); |
| const DlColor color = DlColor::kDarkGrey(); |
| const SkScalar elevation = 5; |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| DisplayListCanvasDispatcher::DrawShadow(canvas, path, color, |
| elevation, false, 1.0); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawShadow(path, color, elevation, false, 1.0); |
| }, |
| kDrawShadowFlags) |
| .set_draw_shadows(), |
| CanvasCompareTester::DefaultTolerance.addBoundsPadding(3, 3)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawShadowTransparentOccluder) { |
| SkPath path; |
| path.addRoundRect( |
| { |
| kRenderLeft + 10, |
| kRenderTop, |
| kRenderRight - 10, |
| kRenderBottom - 20, |
| }, |
| kRenderCornerRadius, kRenderCornerRadius); |
| const DlColor color = DlColor::kDarkGrey(); |
| const SkScalar elevation = 5; |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| DisplayListCanvasDispatcher::DrawShadow(canvas, path, color, |
| elevation, true, 1.0); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawShadow(path, color, elevation, true, 1.0); |
| }, |
| kDrawShadowFlags) |
| .set_draw_shadows(), |
| CanvasCompareTester::DefaultTolerance.addBoundsPadding(3, 3)); |
| } |
| |
| TEST_F(DisplayListCanvas, DrawShadowDpr) { |
| SkPath path; |
| path.addRoundRect( |
| { |
| kRenderLeft + 10, |
| kRenderTop, |
| kRenderRight - 10, |
| kRenderBottom - 20, |
| }, |
| kRenderCornerRadius, kRenderCornerRadius); |
| const DlColor color = DlColor::kDarkGrey(); |
| const SkScalar elevation = 5; |
| |
| CanvasCompareTester::RenderAll( // |
| TestParameters( |
| [=](SkCanvas* canvas, const SkPaint& paint) { // |
| DisplayListCanvasDispatcher::DrawShadow(canvas, path, color, |
| elevation, false, 1.5); |
| }, |
| [=](DisplayListBuilder& builder) { // |
| builder.drawShadow(path, color, elevation, false, 1.5); |
| }, |
| kDrawShadowFlags) |
| .set_draw_shadows(), |
| CanvasCompareTester::DefaultTolerance.addBoundsPadding(3, 3)); |
| } |
| |
| TEST_F(DisplayListCanvas, SaveLayerConsolidation) { |
| float commutable_color_matrix[]{ |
| // clang-format off |
| 0, 1, 0, 0, 0, |
| 0, 0, 1, 0, 0, |
| 1, 0, 0, 0, 0, |
| 0, 0, 0, 1, 0, |
| // clang-format on |
| }; |
| float non_commutable_color_matrix[]{ |
| // clang-format off |
| 0, 1, 0, .1, 0, |
| 0, 0, 1, .1, 0, |
| 1, 0, 0, .1, 0, |
| 0, 0, 0, .7, 0, |
| // clang-format on |
| }; |
| SkMatrix contract_matrix; |
| contract_matrix.setScale(0.9f, 0.9f, kRenderCenterX, kRenderCenterY); |
| |
| std::vector<SkScalar> opacities = { |
| 0, |
| 0.5f, |
| SK_Scalar1, |
| }; |
| std::vector<std::shared_ptr<DlColorFilter>> color_filters = { |
| std::make_shared<DlBlendColorFilter>(DlColor::kCyan(), |
| DlBlendMode::kSrcATop), |
| std::make_shared<DlMatrixColorFilter>(commutable_color_matrix), |
| std::make_shared<DlMatrixColorFilter>(non_commutable_color_matrix), |
| DlSrgbToLinearGammaColorFilter::instance, |
| DlLinearToSrgbGammaColorFilter::instance, |
| }; |
| std::vector<std::shared_ptr<DlImageFilter>> image_filters = { |
| std::make_shared<DlBlurImageFilter>(5.0f, 5.0f, DlTileMode::kDecal), |
| std::make_shared<DlDilateImageFilter>(5.0f, 5.0f), |
| std::make_shared<DlErodeImageFilter>(5.0f, 5.0f), |
| std::make_shared<DlMatrixImageFilter>(contract_matrix, |
| DlImageSampling::kLinear), |
| }; |
| std::vector<std::unique_ptr<RenderEnvironment>> environments; |
| for (auto& provider : CanvasCompareTester::kTestProviders) { |
| auto env = std::make_unique<RenderEnvironment>( |
| provider.get(), PixelFormat::kN32Premul_PixelFormat); |
| environments.push_back(std::move(env)); |
| } |
| |
| auto render_content = [](DisplayListBuilder& builder) { |
| builder.DrawRect( |
| SkRect{kRenderLeft, kRenderTop, kRenderCenterX, kRenderCenterY}, |
| DlPaint(DlColor::kYellow())); |
| builder.DrawRect( |
| SkRect{kRenderCenterX, kRenderTop, kRenderRight, kRenderCenterY}, |
| DlPaint(DlColor::kRed())); |
| builder.DrawRect( |
| SkRect{kRenderLeft, kRenderCenterY, kRenderCenterX, kRenderBottom}, |
| DlPaint(DlColor::kBlue())); |
| builder.DrawRect( |
| SkRect{kRenderCenterX, kRenderCenterY, kRenderRight, kRenderBottom}, |
| DlPaint(DlColor::kRed().modulateOpacity(0.5f))); |
| }; |
| |
| auto test_attributes_env = |
| [render_content](DlPaint& paint1, DlPaint& paint2, |
| const DlPaint& paint_both, bool same, bool rev_same, |
| const std::string& desc1, const std::string& desc2, |
| const RenderEnvironment* env) { |
| DisplayListBuilder nested_builder; |
| nested_builder.SaveLayer(&kTestBounds, &paint1); |
| nested_builder.SaveLayer(&kTestBounds, &paint2); |
| render_content(nested_builder); |
| auto nested_results = env->getResult(nested_builder.Build()); |
| |
| DisplayListBuilder reverse_builder; |
| reverse_builder.SaveLayer(&kTestBounds, &paint2); |
| reverse_builder.SaveLayer(&kTestBounds, &paint1); |
| render_content(reverse_builder); |
| auto reverse_results = env->getResult(reverse_builder.Build()); |
| |
| DisplayListBuilder combined_builder; |
| combined_builder.SaveLayer(&kTestBounds, &paint_both); |
| render_content(combined_builder); |
| auto combined_results = env->getResult(combined_builder.Build()); |
| |
| // Set this boolean to true to test if combinations that are marked |
| // as incompatible actually are compatible despite our predictions. |
| // Some of the combinations that we treat as incompatible actually |
| // are compatible with swapping the order of the operations, but |
| // it would take a bit of new infrastructure to really identify |
| // those combinations. The only hard constraint to test here is |
| // when we claim that they are compatible and they aren't. |
| const bool always = false; |
| |
| if (always || same) { |
| CanvasCompareTester::quickCompareToReference( |
| nested_results.get(), combined_results.get(), same, |
| "nested " + desc1 + " then " + desc2); |
| } |
| if (always || rev_same) { |
| CanvasCompareTester::quickCompareToReference( |
| reverse_results.get(), combined_results.get(), rev_same, |
| "nested " + desc2 + " then " + desc1); |
| } |
| }; |
| |
| auto test_attributes = [test_attributes_env, &environments]( |
| DlPaint& paint1, DlPaint& paint2, |
| const DlPaint& paint_both, bool same, |
| bool rev_same, const std::string& desc1, |
| const std::string& desc2) { |
| for (auto& env : environments) { |
| test_attributes_env(paint1, paint2, paint_both, // |
| same, rev_same, desc1, desc2, env.get()); |
| } |
| }; |
| |
| // CF then Opacity should always work. |
| // The reverse sometimes works. |
| for (size_t cfi = 0; cfi < color_filters.size(); cfi++) { |
| auto color_filter = color_filters[cfi]; |
| std::string cf_desc = "color filter #" + std::to_string(cfi + 1); |
| DlPaint nested_paint1 = DlPaint().setColorFilter(color_filter); |
| |
| for (size_t oi = 0; oi < opacities.size(); oi++) { |
| SkScalar opacity = opacities[oi]; |
| std::string op_desc = "opacity " + std::to_string(opacity); |
| DlPaint nested_paint2 = DlPaint().setOpacity(opacity); |
| |
| DlPaint combined_paint = nested_paint1; |
| combined_paint.setOpacity(opacity); |
| |
| bool op_then_cf_works = opacity <= 0.0 || opacity >= 1.0 || |
| color_filter->can_commute_with_opacity(); |
| |
| test_attributes(nested_paint1, nested_paint2, combined_paint, true, |
| op_then_cf_works, cf_desc, op_desc); |
| } |
| } |
| |
| // Opacity then IF should always work. |
| // The reverse can also work for some values of opacity. |
| // The reverse should also theoretically work for some IFs, but we |
| // get some rounding errors that are more than just trivial. |
| for (size_t oi = 0; oi < opacities.size(); oi++) { |
| SkScalar opacity = opacities[oi]; |
| std::string op_desc = "opacity " + std::to_string(opacity); |
| DlPaint nested_paint1 = DlPaint().setOpacity(opacity); |
| |
| for (size_t ifi = 0; ifi < image_filters.size(); ifi++) { |
| auto image_filter = image_filters[ifi]; |
| std::string if_desc = "image filter #" + std::to_string(ifi + 1); |
| DlPaint nested_paint2 = DlPaint().setImageFilter(image_filter); |
| |
| DlPaint combined_paint = nested_paint1; |
| combined_paint.setImageFilter(image_filter); |
| |
| bool if_then_op_works = opacity <= 0.0 || opacity >= 1.0; |
| test_attributes(nested_paint1, nested_paint2, combined_paint, true, |
| if_then_op_works, op_desc, if_desc); |
| } |
| } |
| |
| // CF then IF should always work. |
| // The reverse might work, but we lack the infrastructure to check it. |
| for (size_t cfi = 0; cfi < color_filters.size(); cfi++) { |
| auto color_filter = color_filters[cfi]; |
| std::string cf_desc = "color filter #" + std::to_string(cfi + 1); |
| DlPaint nested_paint1 = DlPaint().setColorFilter(color_filter); |
| |
| for (size_t ifi = 0; ifi < image_filters.size(); ifi++) { |
| auto image_filter = image_filters[ifi]; |
| std::string if_desc = "image filter #" + std::to_string(ifi + 1); |
| DlPaint nested_paint2 = DlPaint().setImageFilter(image_filter); |
| |
| DlPaint combined_paint = nested_paint1; |
| combined_paint.setImageFilter(image_filter); |
| |
| test_attributes(nested_paint1, nested_paint2, combined_paint, true, false, |
| cf_desc, if_desc); |
| } |
| } |
| } |
| |
| } // namespace testing |
| } // namespace flutter |