| // 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/dl_builder.h" |
| #include "flutter/display_list/dl_op_flags.h" |
| #include "flutter/display_list/dl_sampling_options.h" |
| #include "flutter/display_list/skia/dl_sk_canvas.h" |
| #include "flutter/display_list/skia/dl_sk_conversions.h" |
| #include "flutter/display_list/skia/dl_sk_dispatcher.h" |
| #include "flutter/display_list/testing/dl_test_snippets.h" |
| #include "flutter/display_list/testing/dl_test_surface_provider.h" |
| #include "flutter/display_list/utils/dl_comparable.h" |
| #include "flutter/fml/file.h" |
| #include "flutter/fml/math.h" |
| #include "flutter/testing/display_list_testing.h" |
| #include "flutter/testing/testing.h" |
| #ifdef IMPELLER_SUPPORTS_RENDERING |
| #include "flutter/impeller/typographer/backends/skia/text_frame_skia.h" |
| #endif // IMPELLER_SUPPORTS_RENDERING |
| |
| #include "third_party/skia/include/core/SkBBHFactory.h" |
| #include "third_party/skia/include/core/SkColorFilter.h" |
| #include "third_party/skia/include/core/SkColorSpace.h" |
| #include "third_party/skia/include/core/SkFontMgr.h" |
| #include "third_party/skia/include/core/SkPictureRecorder.h" |
| #include "third_party/skia/include/core/SkStream.h" |
| #include "third_party/skia/include/core/SkSurface.h" |
| #include "third_party/skia/include/core/SkTypeface.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| #include "third_party/skia/include/effects/SkImageFilters.h" |
| #include "third_party/skia/include/encode/SkPngEncoder.h" |
| #include "third_party/skia/include/gpu/GrDirectContext.h" |
| #include "third_party/skia/include/gpu/GrRecordingContext.h" |
| #include "third_party/skia/include/gpu/GrTypes.h" |
| #include "txt/platform.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 kTestBounds2 = 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])); |
| |
| class SkImageSampling { |
| public: |
| static constexpr SkSamplingOptions kNearestNeighbor = |
| SkSamplingOptions(SkFilterMode::kNearest); |
| static constexpr SkSamplingOptions kLinear = |
| SkSamplingOptions(SkFilterMode::kLinear); |
| static constexpr SkSamplingOptions kMipmapLinear = |
| SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear); |
| static constexpr SkSamplingOptions kCubic = |
| SkSamplingOptions(SkCubicResampler{1 / 3.0f, 1 / 3.0f}); |
| }; |
| |
| static void DrawCheckerboard(DlCanvas* canvas) { |
| DlPaint p0, p1; |
| p0.setDrawStyle(DlDrawStyle::kFill); |
| p0.setColor(DlColor(0xff00fe00)); // off-green |
| p1.setDrawStyle(DlDrawStyle::kFill); |
| p1.setColor(DlColor::kBlue()); |
| // Some pixels need some transparency for DstIn testing |
| p1.setAlpha(128); |
| int cbdim = 5; |
| int width = canvas->GetBaseLayerSize().width(); |
| int height = canvas->GetBaseLayerSize().height(); |
| for (int y = 0; y < width; y += cbdim) { |
| for (int x = 0; x < height; x += cbdim) { |
| DlPaint& cellp = ((x + y) & 1) == 0 ? p0 : p1; |
| canvas->DrawRect(SkRect::MakeXYWH(x, y, cbdim, cbdim), cellp); |
| } |
| } |
| } |
| |
| static void DrawCheckerboard(SkCanvas* canvas) { |
| DlSkCanvasAdapter dl_canvas(canvas); |
| DrawCheckerboard(&dl_canvas); |
| } |
| |
| static std::shared_ptr<DlImageColorSource> MakeColorSource( |
| const sk_sp<DlImage>& image) { |
| return std::make_shared<DlImageColorSource>(image, // |
| DlTileMode::kRepeat, // |
| DlTileMode::kRepeat, // |
| DlImageSampling::kLinear); |
| } |
| |
| static sk_sp<SkShader> MakeColorSource(const sk_sp<SkImage>& image) { |
| return image->makeShader(SkTileMode::kRepeat, // |
| SkTileMode::kRepeat, // |
| SkImageSampling::kLinear); |
| } |
| |
| // Used to show "INFO" warnings about tests that are omitted on certain |
| // backends, but only once for the entire test run to avoid warning spam |
| class OncePerBackendWarning { |
| public: |
| explicit OncePerBackendWarning(const std::string& warning) |
| : warning_(warning) {} |
| |
| void warn(const std::string& name) { |
| if (warnings_sent_.find(name) == warnings_sent_.end()) { |
| warnings_sent_.insert(name); |
| FML_LOG(INFO) << warning_ << " on " << name; |
| } |
| } |
| |
| private: |
| std::string warning_; |
| std::set<std::string> warnings_sent_; |
| }; |
| |
| // 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_; } |
| |
| bool operator==(BoundsTolerance const& other) const { |
| return bounds_pad_ == other.bounds_pad_ && scale_ == other.scale_ && |
| absolute_pad_ == other.absolute_pad_ && clip_ == other.clip_ && |
| clip_pad_ == other.clip_pad_ && |
| discrete_offset_ == other.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; |
| }; |
| |
| template <typename C, typename P, typename I> |
| struct RenderContext { |
| C canvas; |
| P paint; |
| I image; |
| }; |
| using SkSetupContext = RenderContext<SkCanvas*, SkPaint&, sk_sp<SkImage>>; |
| using DlSetupContext = RenderContext<DlCanvas*, DlPaint&, sk_sp<DlImage>>; |
| using SkRenderContext = |
| RenderContext<SkCanvas*, const SkPaint&, sk_sp<SkImage>>; |
| using DlRenderContext = |
| RenderContext<DlCanvas*, const DlPaint&, sk_sp<DlImage>>; |
| |
| using SkSetup = const std::function<void(const SkSetupContext&)>; |
| using SkRenderer = const std::function<void(const SkRenderContext&)>; |
| using DlSetup = const std::function<void(const DlSetupContext&)>; |
| using DlRenderer = const std::function<void(const DlRenderContext&)>; |
| static const SkSetup kEmptySkSetup = [](const SkSetupContext&) {}; |
| static const SkRenderer kEmptySkRenderer = [](const SkRenderContext&) {}; |
| static const DlSetup kEmptyDlSetup = [](const DlSetupContext&) {}; |
| static const DlRenderer kEmptyDlRenderer = [](const DlRenderContext&) {}; |
| |
| using PixelFormat = DlSurfaceProvider::PixelFormat; |
| using BackendType = DlSurfaceProvider::BackendType; |
| |
| class RenderResult { |
| public: |
| virtual ~RenderResult() = default; |
| |
| virtual sk_sp<SkImage> image() const = 0; |
| virtual int width() const = 0; |
| virtual int height() const = 0; |
| virtual const uint32_t* addr32(int x, int y) const = 0; |
| virtual void write(const std::string& path) const = 0; |
| }; |
| |
| class SkRenderResult final : public RenderResult { |
| public: |
| explicit SkRenderResult(const sk_sp<SkSurface>& surface, |
| bool take_snapshot = false) { |
| SkImageInfo info = surface->imageInfo(); |
| info = SkImageInfo::MakeN32Premul(info.dimensions()); |
| addr_ = malloc(info.computeMinByteSize() * info.height()); |
| pixmap_.reset(info, addr_, info.minRowBytes()); |
| surface->readPixels(pixmap_, 0, 0); |
| if (take_snapshot) { |
| image_ = surface->makeImageSnapshot(); |
| } |
| } |
| ~SkRenderResult() override { free(addr_); } |
| |
| sk_sp<SkImage> image() const override { return image_; } |
| int width() const override { return pixmap_.width(); } |
| int height() const override { return pixmap_.height(); } |
| const uint32_t* addr32(int x, int y) const override { |
| return pixmap_.addr32(x, y); |
| } |
| void write(const std::string& path) const { |
| auto stream = SkFILEWStream(path.c_str()); |
| SkPngEncoder::Options options; |
| SkPngEncoder::Encode(&stream, pixmap_, options); |
| stream.flush(); |
| } |
| |
| private: |
| sk_sp<SkImage> image_; |
| SkPixmap pixmap_; |
| void* addr_ = nullptr; |
| }; |
| |
| class ImpellerRenderResult final : public RenderResult { |
| public: |
| explicit ImpellerRenderResult(sk_sp<DlPixelData> screenshot, |
| SkRect render_bounds) |
| : screenshot_(std::move(screenshot)), render_bounds_(render_bounds) {} |
| ~ImpellerRenderResult() override = default; |
| |
| sk_sp<SkImage> image() const override { return nullptr; }; |
| int width() const override { return screenshot_->width(); }; |
| int height() const override { return screenshot_->height(); } |
| const uint32_t* addr32(int x, int y) const override { |
| return screenshot_->addr32(x, y); |
| } |
| void write(const std::string& path) const override { |
| screenshot_->write(path); |
| } |
| const SkRect& render_bounds() const { return render_bounds_; } |
| |
| private: |
| const sk_sp<DlPixelData> screenshot_; |
| SkRect render_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) = 0; |
| virtual bool targets_impeller() const { return false; } |
| }; |
| |
| struct MatrixClipJobRenderer : public JobRenderer { |
| public: |
| const SkMatrix& setup_matrix() const { |
| FML_CHECK(is_setup_); |
| return setup_matrix_; |
| } |
| |
| const SkIRect& setup_clip_bounds() const { |
| FML_CHECK(is_setup_); |
| return setup_clip_bounds_; |
| } |
| |
| protected: |
| bool is_setup_ = false; |
| SkMatrix setup_matrix_; |
| SkIRect setup_clip_bounds_; |
| }; |
| |
| struct SkJobRenderer : public MatrixClipJobRenderer { |
| explicit SkJobRenderer(const SkSetup& sk_setup, |
| const SkRenderer& sk_render, |
| const SkRenderer& sk_restore, |
| const sk_sp<SkImage>& sk_image) |
| : sk_setup_(sk_setup), |
| sk_render_(sk_render), |
| sk_restore_(sk_restore), |
| sk_image_(sk_image) {} |
| |
| void Render(SkCanvas* canvas, const RenderJobInfo& info) override { |
| FML_DCHECK(info.opacity == SK_Scalar1); |
| SkPaint paint; |
| sk_setup_({canvas, paint, sk_image_}); |
| setup_paint_ = paint; |
| setup_matrix_ = canvas->getTotalMatrix(); |
| setup_clip_bounds_ = canvas->getDeviceClipBounds(); |
| is_setup_ = true; |
| sk_render_({canvas, paint, sk_image_}); |
| sk_restore_({canvas, paint, sk_image_}); |
| } |
| |
| sk_sp<SkPicture> MakePicture(const RenderJobInfo& info) { |
| SkPictureRecorder recorder; |
| SkRTreeFactory rtree_factory; |
| SkCanvas* cv = recorder.beginRecording(kTestBounds2, &rtree_factory); |
| Render(cv, info); |
| return recorder.finishRecordingAsPicture(); |
| } |
| |
| const SkPaint& setup_paint() const { |
| FML_CHECK(is_setup_); |
| return setup_paint_; |
| } |
| |
| private: |
| const SkSetup sk_setup_; |
| const SkRenderer sk_render_; |
| const SkRenderer sk_restore_; |
| sk_sp<SkImage> sk_image_; |
| SkPaint setup_paint_; |
| }; |
| |
| struct DlJobRenderer : public MatrixClipJobRenderer { |
| explicit DlJobRenderer(const DlSetup& dl_setup, |
| const DlRenderer& dl_render, |
| const DlRenderer& dl_restore, |
| const sk_sp<DlImage>& dl_image) |
| : dl_setup_(dl_setup), |
| dl_render_(dl_render), |
| dl_restore_(dl_restore), |
| dl_image_(dl_image) {} |
| |
| void Render(SkCanvas* sk_canvas, const RenderJobInfo& info) override { |
| DlSkCanvasAdapter canvas(sk_canvas); |
| Render(&canvas, info); |
| } |
| |
| void Render(DlCanvas* canvas, const RenderJobInfo& info) { |
| FML_DCHECK(info.opacity == SK_Scalar1); |
| DlPaint paint; |
| dl_setup_({canvas, paint, dl_image_}); |
| setup_paint_ = paint; |
| setup_matrix_ = canvas->GetTransform(); |
| setup_clip_bounds_ = canvas->GetDestinationClipBounds().roundOut(); |
| is_setup_ = true; |
| dl_render_({canvas, paint, dl_image_}); |
| dl_restore_({canvas, paint, dl_image_}); |
| } |
| |
| sk_sp<DisplayList> MakeDisplayList(const RenderJobInfo& info) { |
| DisplayListBuilder builder(kTestBounds2); |
| Render(&builder, info); |
| return builder.Build(); |
| } |
| |
| const DlPaint& setup_paint() const { |
| FML_CHECK(is_setup_); |
| return setup_paint_; |
| } |
| |
| bool targets_impeller() const override { |
| return dl_image_->impeller_texture() != nullptr; |
| } |
| |
| private: |
| const DlSetup dl_setup_; |
| const DlRenderer dl_render_; |
| const DlRenderer dl_restore_; |
| const sk_sp<DlImage> dl_image_; |
| DlPaint setup_paint_; |
| }; |
| |
| struct SkPictureJobRenderer : public JobRenderer { |
| explicit SkPictureJobRenderer(sk_sp<SkPicture> picture) |
| : picture_(std::move(picture)) {} |
| |
| void Render(SkCanvas* canvas, const RenderJobInfo& info) { |
| FML_DCHECK(info.opacity == SK_Scalar1); |
| picture_->playback(canvas); |
| } |
| |
| private: |
| sk_sp<SkPicture> picture_; |
| }; |
| |
| struct DisplayListJobRenderer : public JobRenderer { |
| explicit DisplayListJobRenderer(sk_sp<DisplayList> display_list) |
| : display_list_(std::move(display_list)) {} |
| |
| void Render(SkCanvas* canvas, const RenderJobInfo& info) { |
| DlSkCanvasAdapter(canvas).DrawDisplayList(display_list_, info.opacity); |
| } |
| |
| private: |
| sk_sp<DisplayList> display_list_; |
| }; |
| |
| class RenderEnvironment { |
| public: |
| static bool EnableImpeller; |
| |
| 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::k565PixelFormat); |
| } |
| |
| static RenderEnvironment MakeN32(const DlSurfaceProvider* provider) { |
| return RenderEnvironment(provider, PixelFormat::kN32PremulPixelFormat); |
| } |
| |
| void init_ref(SkSetup& sk_setup, |
| SkRenderer& sk_renderer, |
| DlSetup& dl_setup, |
| DlRenderer& dl_renderer, |
| DlRenderer& imp_renderer, |
| DlColor bg = DlColor::kTransparent()) { |
| SkJobRenderer sk_job(sk_setup, sk_renderer, kEmptySkRenderer, kTestSkImage); |
| RenderJobInfo info = { |
| .bg = bg, |
| }; |
| ref_sk_result_ = getResult(info, sk_job); |
| DlJobRenderer dl_job(dl_setup, dl_renderer, kEmptyDlRenderer, kTestDlImage); |
| ref_dl_result_ = getResult(info, dl_job); |
| ref_dl_paint_ = dl_job.setup_paint(); |
| ref_matrix_ = dl_job.setup_matrix(); |
| ref_clip_bounds_ = dl_job.setup_clip_bounds(); |
| ASSERT_EQ(sk_job.setup_matrix(), ref_matrix_); |
| ASSERT_EQ(sk_job.setup_clip_bounds(), ref_clip_bounds_); |
| if (supports_impeller()) { |
| test_impeller_image_ = makeTestImpellerImage(provider_); |
| DlJobRenderer imp_job(dl_setup, imp_renderer, kEmptyDlRenderer, |
| test_impeller_image_); |
| ref_impeller_result_ = getImpellerResult(info, imp_job); |
| } |
| } |
| |
| 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(ToSk(info.bg)); |
| |
| int restore_count = canvas->save(); |
| canvas->scale(info.scale, info.scale); |
| renderer.Render(canvas, info); |
| canvas->restoreToCount(restore_count); |
| |
| if (GrDirectContext* dContext = |
| GrAsDirectContext(surface->recordingContext())) { |
| dContext->flushAndSubmit(surface.get(), GrSyncCpu::kYes); |
| } |
| return std::make_unique<SkRenderResult>(surface); |
| } |
| |
| std::unique_ptr<RenderResult> getResult(sk_sp<DisplayList> dl) const { |
| DisplayListJobRenderer job(std::move(dl)); |
| RenderJobInfo info = {}; |
| return getResult(info, job); |
| } |
| |
| std::unique_ptr<ImpellerRenderResult> getImpellerResult( |
| const RenderJobInfo& info, |
| DlJobRenderer& renderer) const { |
| FML_DCHECK(info.scale == SK_Scalar1); |
| |
| DisplayListBuilder builder; |
| builder.Clear(info.bg); |
| auto render_dl = renderer.MakeDisplayList(info); |
| builder.DrawDisplayList(render_dl); |
| auto dl = builder.Build(); |
| auto snap = provider_->ImpellerSnapshot(dl, kTestWidth, kTestHeight); |
| return std::make_unique<ImpellerRenderResult>(std::move(snap), |
| render_dl->bounds()); |
| } |
| |
| const DlSurfaceProvider* provider() const { return provider_; } |
| bool valid() const { return provider_->supports(format_); } |
| const std::string backend_name() const { return provider_->backend_name(); } |
| bool supports_impeller() const { |
| return EnableImpeller && provider_->supports_impeller(); |
| } |
| |
| PixelFormat format() const { return format_; } |
| const DlPaint& ref_dl_paint() const { return ref_dl_paint_; } |
| const SkMatrix& ref_matrix() const { return ref_matrix_; } |
| const SkIRect& ref_clip_bounds() const { return ref_clip_bounds_; } |
| const RenderResult* ref_sk_result() const { return ref_sk_result_.get(); } |
| const RenderResult* ref_dl_result() const { return ref_dl_result_.get(); } |
| const ImpellerRenderResult* ref_impeller_result() const { |
| return ref_impeller_result_.get(); |
| } |
| |
| const sk_sp<SkImage> sk_image() const { return kTestSkImage; } |
| const sk_sp<DlImage> dl_image() const { return kTestDlImage; } |
| const sk_sp<DlImage> impeller_image() const { return test_impeller_image_; } |
| |
| 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_; |
| |
| DlPaint ref_dl_paint_; |
| SkMatrix ref_matrix_; |
| SkIRect ref_clip_bounds_; |
| std::unique_ptr<RenderResult> ref_sk_result_; |
| std::unique_ptr<RenderResult> ref_dl_result_; |
| std::unique_ptr<ImpellerRenderResult> ref_impeller_result_; |
| sk_sp<DlImage> test_impeller_image_; |
| |
| static const sk_sp<SkImage> kTestSkImage; |
| static const sk_sp<DlImage> kTestDlImage; |
| static const sk_sp<SkImage> makeTestSkImage() { |
| sk_sp<SkSurface> surface = SkSurfaces::Raster( |
| SkImageInfo::MakeN32Premul(kRenderWidth, kRenderHeight)); |
| DrawCheckerboard(surface->getCanvas()); |
| return surface->makeImageSnapshot(); |
| } |
| static const sk_sp<DlImage> makeTestImpellerImage( |
| const DlSurfaceProvider* provider) { |
| FML_DCHECK(provider->supports_impeller()); |
| DisplayListBuilder builder(SkRect::MakeWH(kRenderWidth, kRenderHeight)); |
| DrawCheckerboard(&builder); |
| return provider->MakeImpellerImage(builder.Build(), // |
| kRenderWidth, kRenderHeight); |
| } |
| }; |
| |
| const sk_sp<SkImage> RenderEnvironment::kTestSkImage = makeTestSkImage(); |
| const sk_sp<DlImage> RenderEnvironment::kTestDlImage = |
| DlImage::Make(kTestSkImage); |
| |
| class CaseParameters { |
| public: |
| explicit CaseParameters(std::string info) |
| : CaseParameters(std::move(info), kEmptySkSetup, kEmptyDlSetup) {} |
| |
| CaseParameters(std::string info, SkSetup& sk_setup, DlSetup& dl_setup) |
| : CaseParameters(std::move(info), |
| sk_setup, |
| dl_setup, |
| kEmptySkRenderer, |
| kEmptyDlRenderer, |
| DlColor(SK_ColorTRANSPARENT), |
| false, |
| false, |
| false) {} |
| |
| CaseParameters(std::string info, |
| SkSetup& sk_setup, |
| DlSetup& dl_setup, |
| SkRenderer& sk_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), |
| sk_setup_(sk_setup), |
| dl_setup_(dl_setup), |
| sk_restore_(sk_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(SkRenderer& sk_restore, |
| DlRenderer& dl_restore, |
| bool mutating_layer, |
| bool fuzzy_compare_components = false) { |
| return CaseParameters(info_, sk_setup_, dl_setup_, sk_restore, dl_restore, |
| bg_, has_diff_clip_, mutating_layer, |
| fuzzy_compare_components); |
| } |
| |
| CaseParameters with_bg(DlColor bg) { |
| return CaseParameters(info_, sk_setup_, dl_setup_, sk_restore_, dl_restore_, |
| bg, has_diff_clip_, has_mutating_save_layer_, |
| fuzzy_compare_components_); |
| } |
| |
| CaseParameters with_diff_clip() { |
| return CaseParameters(info_, sk_setup_, dl_setup_, sk_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_; } |
| |
| SkSetup sk_setup() const { return sk_setup_; } |
| DlSetup dl_setup() const { return dl_setup_; } |
| SkRenderer sk_restore() const { return sk_restore_; } |
| DlRenderer dl_restore() const { return dl_restore_; } |
| |
| private: |
| const std::string info_; |
| const DlColor bg_; |
| const SkSetup sk_setup_; |
| const DlSetup dl_setup_; |
| const SkRenderer sk_restore_; |
| const DlRenderer dl_restore_; |
| const bool has_diff_clip_; |
| const bool has_mutating_save_layer_; |
| const bool fuzzy_compare_components_; |
| }; |
| |
| class TestParameters { |
| public: |
| TestParameters(const SkRenderer& sk_renderer, |
| const DlRenderer& dl_renderer, |
| const DisplayListAttributeFlags& flags) |
| : TestParameters(sk_renderer, dl_renderer, dl_renderer, flags) {} |
| |
| TestParameters(const SkRenderer& sk_renderer, |
| const DlRenderer& dl_renderer, |
| const DlRenderer& imp_renderer, |
| const DisplayListAttributeFlags& flags) |
| : sk_renderer_(sk_renderer), |
| dl_renderer_(dl_renderer), |
| imp_renderer_(imp_renderer), |
| flags_(flags) {} |
| |
| bool uses_paint() const { return !flags_.ignores_paint(); } |
| bool uses_gradient() const { return flags_.applies_shader(); } |
| |
| bool impeller_compatible(const DlPaint& paint) const { |
| if (is_draw_text_blob()) { |
| // Non-color text is rendered as paths |
| if (paint.getColorSourcePtr() && !paint.getColorSourcePtr()->asColor()) { |
| return false; |
| } |
| // Non-filled text (stroke or stroke and fill) is rendered as paths |
| if (paint.getDrawStyle() != DlDrawStyle::kFill) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool should_match(const RenderEnvironment& env, |
| const CaseParameters& caseP, |
| const DlPaint& attr, |
| const MatrixClipJobRenderer& renderer) const { |
| if (caseP.has_mutating_save_layer()) { |
| return false; |
| } |
| if (env.ref_clip_bounds() != renderer.setup_clip_bounds() || |
| caseP.has_diff_clip()) { |
| return false; |
| } |
| if (env.ref_matrix() != renderer.setup_matrix() && !flags_.is_flood()) { |
| return false; |
| } |
| if (flags_.ignores_paint()) { |
| return true; |
| } |
| const DlPaint& ref_attr = env.ref_dl_paint(); |
| if (flags_.applies_anti_alias() && // |
| ref_attr.isAntiAlias() != attr.isAntiAlias()) { |
| if (renderer.targets_impeller()) { |
| // Impeller only does MSAA, ignoring the AA attribute |
| // https://github.com/flutter/flutter/issues/104721 |
| } else { |
| return false; |
| } |
| } |
| if (flags_.applies_color() && // |
| ref_attr.getColor() != attr.getColor()) { |
| return false; |
| } |
| if (flags_.applies_blend() && // |
| ref_attr.getBlendMode() != attr.getBlendMode()) { |
| 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; |
| } |
| |
| bool is_stroked = flags_.is_stroked(attr.getDrawStyle()); |
| if (flags_.is_stroked(ref_attr.getDrawStyle()) != is_stroked) { |
| return false; |
| } |
| DisplayListSpecialGeometryFlags geo_flags = |
| flags_.WithPathEffect(attr.getPathEffect().get(), is_stroked); |
| if (flags_.applies_path_effect() && // |
| ref_attr.getPathEffect() != attr.getPathEffect()) { |
| if (renderer.targets_impeller()) { |
| // Impeller ignores DlPathEffect objects: |
| // https://github.com/flutter/flutter/issues/109736 |
| } else { |
| switch (attr.getPathEffect()->type()) { |
| case DlPathEffectType::kDash: { |
| if (is_stroked && !ignores_dashes()) { |
| return false; |
| } |
| break; |
| } |
| } |
| } |
| } |
| 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 DlPaint& 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 DlPaint& 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.getDrawStyle() != DlDrawStyle::kFill && |
| paint.getStrokeJoin() == DlStrokeJoin::kMiter) { |
| // 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 DlPaint& 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 path_effect = paint.getPathEffect(); |
| |
| DisplayListSpecialGeometryFlags geo_flags = |
| flags_.WithPathEffect(path_effect.get(), true); |
| if (paint.getStrokeCap() == DlStrokeCap::kButt && |
| !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 SkRenderer& sk_renderer() const { return sk_renderer_; } |
| const DlRenderer& dl_renderer() const { return dl_renderer_; } |
| const DlRenderer& imp_renderer() const { return imp_renderer_; } |
| |
| // 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_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 SkRenderer sk_renderer_; |
| const DlRenderer dl_renderer_; |
| const DlRenderer imp_renderer_; |
| const DisplayListAttributeFlags flags_; |
| |
| 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 CanvasCompareTester { |
| public: |
| static std::vector<BackendType> TestBackends; |
| static std::string ImpellerFailureImageDirectory; |
| static bool SaveImpellerFailureImages; |
| static std::vector<std::string> ImpellerFailureImages; |
| static bool ImpellerSupported; |
| |
| static std::unique_ptr<DlSurfaceProvider> GetProvider(BackendType type) { |
| auto provider = DlSurfaceProvider::Create(type); |
| if (provider == nullptr) { |
| FML_LOG(ERROR) << "provider " << DlSurfaceProvider::BackendName(type) |
| << " not supported (ignoring)"; |
| return nullptr; |
| } |
| provider->InitializeSurface(kTestWidth, kTestHeight, |
| PixelFormat::kN32PremulPixelFormat); |
| return provider; |
| } |
| |
| static void ClearProviders() { TestBackends.clear(); } |
| |
| static bool AddProvider(BackendType type) { |
| auto provider = GetProvider(type); |
| if (!provider) { |
| return false; |
| } |
| if (provider->supports_impeller()) { |
| ImpellerSupported = true; |
| } |
| TestBackends.push_back(type); |
| return true; |
| } |
| |
| static BoundsTolerance DefaultTolerance; |
| |
| static void RenderAll(const TestParameters& params, |
| const BoundsTolerance& tolerance = DefaultTolerance) { |
| for (auto& back_end : TestBackends) { |
| auto provider = GetProvider(back_end); |
| RenderEnvironment env = RenderEnvironment::MakeN32(provider.get()); |
| env.init_ref(kEmptySkSetup, params.sk_renderer(), // |
| kEmptyDlSetup, params.dl_renderer(), params.imp_renderer()); |
| quickCompareToReference(env, "default"); |
| if (env.supports_impeller()) { |
| auto impeller_result = env.ref_impeller_result(); |
| if (!checkPixels(impeller_result, impeller_result->render_bounds(), |
| "Impeller reference")) { |
| std::string test_name = |
| ::testing::UnitTest::GetInstance()->current_test_info()->name(); |
| save_to_png(impeller_result, test_name + " (Impeller reference)", |
| "base rendering was blank or out of bounds"); |
| } |
| } else { |
| static OncePerBackendWarning warnings("No Impeller output tests"); |
| warnings.warn(env.backend_name()); |
| } |
| |
| 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); |
| SkRenderer sk_safe_restore = [=](const SkRenderContext& ctx) { |
| // Draw another primitive to disable peephole optimizations |
| ctx.canvas->drawRect(kRenderBounds.makeOffset(500, 500), SkPaint()); |
| ctx.canvas->restore(); |
| }; |
| DlRenderer dl_safe_restore = [=](const DlRenderContext& ctx) { |
| // Draw another primitive to disable peephole optimizations |
| // As the rendering op rejection in the DisplayList Builder |
| // gets smarter and smarter, this operation has had to get |
| // sneakier and sneakier about specifying an operation that |
| // won't practically show up in the output, but technically |
| // can't be culled. |
| ctx.canvas->DrawRect( |
| SkRect::MakeXYWH(kRenderCenterX, kRenderCenterY, 0.0001, 0.0001), |
| DlPaint()); |
| ctx.canvas->Restore(); |
| }; |
| SkRenderer sk_opt_restore = [=](const SkRenderContext& ctx) { |
| // Just a simple restore to allow peephole optimizations to occur |
| ctx.canvas->restore(); |
| }; |
| DlRenderer dl_opt_restore = [=](const DlRenderContext& ctx) { |
| // Just a simple restore to allow peephole optimizations to occur |
| ctx.canvas->Restore(); |
| }; |
| SkRect layer_bounds = kRenderBounds.makeInset(15, 15); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "With prior save/clip/restore", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->save(); |
| ctx.canvas->clipRect(clip, SkClipOp::kIntersect, false); |
| SkPaint p2; |
| ctx.canvas->drawRect(rect, p2); |
| p2.setBlendMode(SkBlendMode::kClear); |
| ctx.canvas->drawRect(rect, p2); |
| ctx.canvas->restore(); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->Save(); |
| ctx.canvas->ClipRect(clip, ClipOp::kIntersect, false); |
| DlPaint p2; |
| ctx.canvas->DrawRect(rect, p2); |
| p2.setBlendMode(DlBlendMode::kClear); |
| ctx.canvas->DrawRect(rect, p2); |
| ctx.canvas->Restore(); |
| })); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer no paint, no bounds", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->saveLayer(nullptr, nullptr); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->SaveLayer(nullptr, nullptr); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, false)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer no paint, with bounds", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->saveLayer(layer_bounds, nullptr); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->SaveLayer(&layer_bounds, nullptr); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer with alpha, no bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setColor(ToSk(alpha_layer_color)); |
| ctx.canvas->saveLayer(nullptr, &save_p); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setColor(alpha_layer_color); |
| ctx.canvas->SaveLayer(nullptr, &save_p); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer with peephole alpha, no bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setColor(ToSk(alpha_layer_color)); |
| ctx.canvas->saveLayer(nullptr, &save_p); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setColor(alpha_layer_color); |
| ctx.canvas->SaveLayer(nullptr, &save_p); |
| }) |
| .with_restore(sk_opt_restore, dl_opt_restore, true, true)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer with alpha and bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setColor(ToSk(alpha_layer_color)); |
| ctx.canvas->saveLayer(layer_bounds, &save_p); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setColor(alpha_layer_color); |
| ctx.canvas->SaveLayer(&layer_bounds, &save_p); |
| }) |
| .with_restore(sk_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()); |
| SkSetup sk_backdrop_setup = [=](const SkSetupContext& ctx) { |
| SkPaint setup_p; |
| setup_p.setShader(MakeColorSource(ctx.image)); |
| ctx.canvas->drawPaint(setup_p); |
| }; |
| DlSetup dl_backdrop_setup = [=](const DlSetupContext& ctx) { |
| DlPaint setup_p; |
| setup_p.setColorSource(MakeColorSource(ctx.image)); |
| ctx.canvas->DrawPaint(setup_p); |
| }; |
| SkSetup sk_content_setup = [=](const SkSetupContext& ctx) { |
| ctx.paint.setAlpha(ctx.paint.getAlpha() / 2); |
| }; |
| DlSetup dl_content_setup = [=](const DlSetupContext& ctx) { |
| ctx.paint.setAlpha(ctx.paint.getAlpha() / 2); |
| }; |
| backdrop_env.init_ref(sk_backdrop_setup, testP.sk_renderer(), |
| dl_backdrop_setup, testP.dl_renderer(), |
| testP.imp_renderer()); |
| quickCompareToReference(backdrop_env, "backdrop"); |
| |
| DlBlurImageFilter dl_backdrop(5, 5, DlTileMode::kDecal); |
| auto sk_backdrop = |
| SkImageFilters::Blur(5, 5, SkTileMode::kDecal, nullptr); |
| RenderWith(testP, backdrop_env, tolerance, |
| CaseParameters( |
| "saveLayer with backdrop", |
| [=](const SkSetupContext& ctx) { |
| sk_backdrop_setup(ctx); |
| ctx.canvas->saveLayer(SkCanvas::SaveLayerRec( |
| nullptr, nullptr, sk_backdrop.get(), 0)); |
| sk_content_setup(ctx); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_backdrop_setup(ctx); |
| ctx.canvas->SaveLayer(nullptr, nullptr, &dl_backdrop); |
| dl_content_setup(ctx); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, backdrop_env, tolerance, |
| CaseParameters( |
| "saveLayer with bounds and backdrop", |
| [=](const SkSetupContext& ctx) { |
| sk_backdrop_setup(ctx); |
| ctx.canvas->saveLayer(SkCanvas::SaveLayerRec( |
| &layer_bounds, nullptr, sk_backdrop.get(), 0)); |
| sk_content_setup(ctx); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_backdrop_setup(ctx); |
| ctx.canvas->SaveLayer(&layer_bounds, nullptr, |
| &dl_backdrop); |
| dl_content_setup(ctx); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| RenderWith(testP, backdrop_env, tolerance, |
| CaseParameters( |
| "clipped saveLayer with backdrop", |
| [=](const SkSetupContext& ctx) { |
| sk_backdrop_setup(ctx); |
| ctx.canvas->clipRect(layer_bounds); |
| ctx.canvas->saveLayer(SkCanvas::SaveLayerRec( |
| nullptr, nullptr, sk_backdrop.get(), 0)); |
| sk_content_setup(ctx); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_backdrop_setup(ctx); |
| ctx.canvas->ClipRect(layer_bounds); |
| ctx.canvas->SaveLayer(nullptr, nullptr, &dl_backdrop); |
| dl_content_setup(ctx); |
| }) |
| .with_restore(sk_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 dl_alpha_rotate_filter(rotate_alpha_color_matrix); |
| auto sk_alpha_rotate_filter = |
| SkColorFilters::Matrix(rotate_alpha_color_matrix); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ColorFilter, no bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setColorFilter(sk_alpha_rotate_filter); |
| ctx.canvas->saveLayer(nullptr, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setColorFilter(&dl_alpha_rotate_filter); |
| ctx.canvas->SaveLayer(nullptr, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| } |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ColorFilter and bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setColorFilter(sk_alpha_rotate_filter); |
| ctx.canvas->saveLayer(kRenderBounds, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setColorFilter(&dl_alpha_rotate_filter); |
| ctx.canvas->SaveLayer(&kRenderBounds, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| } |
| } |
| |
| { |
| // clang-format off |
| constexpr float color_matrix[20] = { |
| 0.5, 0, 0, 0, 0.5, |
| 0, 0.5, 0, 0, 0.5, |
| 0, 0, 0.5, 0, 0.5, |
| 0, 0, 0, 1, 0, |
| }; |
| // clang-format on |
| DlMatrixColorFilter dl_color_filter(color_matrix); |
| DlColorFilterImageFilter dl_cf_image_filter(dl_color_filter); |
| auto sk_cf_image_filter = SkImageFilters::ColorFilter( |
| SkColorFilters::Matrix(color_matrix), nullptr); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ImageFilter, no bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setImageFilter(sk_cf_image_filter); |
| ctx.canvas->saveLayer(nullptr, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setImageFilter(&dl_cf_image_filter); |
| ctx.canvas->SaveLayer(nullptr, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }) |
| .with_restore(sk_safe_restore, dl_safe_restore, true)); |
| } |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "saveLayer ImageFilter and bounds", |
| [=](const SkSetupContext& ctx) { |
| SkPaint save_p; |
| save_p.setImageFilter(sk_cf_image_filter); |
| ctx.canvas->saveLayer(kRenderBounds, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| DlPaint save_p; |
| save_p.setImageFilter(&dl_cf_image_filter); |
| ctx.canvas->SaveLayer(&kRenderBounds, &save_p); |
| ctx.paint.setStrokeWidth(5.0); |
| }) |
| .with_restore(sk_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); |
| auto sk_aa_setup = [=](SkSetupContext ctx, bool is_aa) { |
| ctx.canvas->translate(0.1, 0.1); |
| ctx.paint.setAntiAlias(is_aa); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| auto dl_aa_setup = [=](DlSetupContext ctx, bool is_aa) { |
| ctx.canvas->Translate(0.1, 0.1); |
| ctx.paint.setAntiAlias(is_aa); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| aa_env.init_ref( |
| [=](const SkSetupContext& ctx) { sk_aa_setup(ctx, false); }, |
| testP.sk_renderer(), |
| [=](const DlSetupContext& ctx) { dl_aa_setup(ctx, false); }, |
| testP.dl_renderer(), testP.imp_renderer()); |
| quickCompareToReference(aa_env, "AntiAlias"); |
| RenderWith( |
| testP, aa_env, aa_tolerance, |
| CaseParameters( |
| "AntiAlias == True", |
| [=](const SkSetupContext& ctx) { sk_aa_setup(ctx, true); }, |
| [=](const DlSetupContext& ctx) { dl_aa_setup(ctx, true); })); |
| RenderWith( |
| testP, aa_env, aa_tolerance, |
| CaseParameters( |
| "AntiAlias == False", |
| [=](const SkSetupContext& ctx) { sk_aa_setup(ctx, false); }, |
| [=](const DlSetupContext& ctx) { dl_aa_setup(ctx, false); })); |
| } |
| |
| RenderWith( // |
| testP, env, tolerance, |
| CaseParameters( |
| "Color == Blue", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setColor(SK_ColorBLUE); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setColor(DlColor::kBlue()); |
| })); |
| RenderWith( // |
| testP, env, tolerance, |
| CaseParameters( |
| "Color == Green", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setColor(SK_ColorGREEN); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setColor(DlColor::kGreen()); |
| })); |
| |
| 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", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setBlendMode(SkBlendMode::kSrcIn); |
| ctx.paint.setColor(blendable_color.argb()); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setBlendMode(DlBlendMode::kSrcIn); |
| ctx.paint.setColor(blendable_color); |
| }) |
| .with_bg(bg)); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Blend == DstIn", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setBlendMode(SkBlendMode::kDstIn); |
| ctx.paint.setColor(blendable_color.argb()); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setBlendMode(DlBlendMode::kDstIn); |
| ctx.paint.setColor(blendable_color); |
| }) |
| .with_bg(bg)); |
| } |
| |
| { |
| // 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()); |
| SkSetup sk_blur_setup = [=](const SkSetupContext& ctx) { |
| ctx.paint.setShader(MakeColorSource(ctx.image)); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| DlSetup dl_blur_setup = [=](const DlSetupContext& ctx) { |
| ctx.paint.setColorSource(MakeColorSource(ctx.image)); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| blur_env.init_ref(sk_blur_setup, testP.sk_renderer(), // |
| dl_blur_setup, testP.dl_renderer(), |
| testP.imp_renderer()); |
| quickCompareToReference(blur_env, "blur"); |
| DlBlurImageFilter dl_filter_decal_5(5.0, 5.0, DlTileMode::kDecal); |
| auto sk_filter_decal_5 = |
| SkImageFilters::Blur(5.0, 5.0, SkTileMode::kDecal, nullptr); |
| BoundsTolerance blur_5_tolerance = tolerance.addBoundsPadding(4, 4); |
| { |
| RenderWith(testP, blur_env, blur_5_tolerance, |
| CaseParameters( |
| "ImageFilter == Decal Blur 5", |
| [=](const SkSetupContext& ctx) { |
| sk_blur_setup(ctx); |
| ctx.paint.setImageFilter(sk_filter_decal_5); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_blur_setup(ctx); |
| ctx.paint.setImageFilter(&dl_filter_decal_5); |
| })); |
| } |
| DlBlurImageFilter dl_filter_clamp_5(5.0, 5.0, DlTileMode::kClamp); |
| auto sk_filter_clamp_5 = |
| SkImageFilters::Blur(5.0, 5.0, SkTileMode::kClamp, nullptr); |
| { |
| RenderWith(testP, blur_env, blur_5_tolerance, |
| CaseParameters( |
| "ImageFilter == Clamp Blur 5", |
| [=](const SkSetupContext& ctx) { |
| sk_blur_setup(ctx); |
| ctx.paint.setImageFilter(sk_filter_clamp_5); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_blur_setup(ctx); |
| ctx.paint.setImageFilter(&dl_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()); |
| SkSetup sk_dilate_setup = [=](const SkSetupContext& ctx) { |
| ctx.paint.setShader(MakeColorSource(ctx.image)); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| DlSetup dl_dilate_setup = [=](const DlSetupContext& ctx) { |
| ctx.paint.setColorSource(MakeColorSource(ctx.image)); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| dilate_env.init_ref(sk_dilate_setup, testP.sk_renderer(), // |
| dl_dilate_setup, testP.dl_renderer(), |
| testP.imp_renderer()); |
| quickCompareToReference(dilate_env, "dilate"); |
| DlDilateImageFilter dl_dilate_filter_5(5.0, 5.0); |
| auto sk_dilate_filter_5 = SkImageFilters::Dilate(5.0, 5.0, nullptr); |
| RenderWith(testP, dilate_env, tolerance, |
| CaseParameters( |
| "ImageFilter == Dilate 5", |
| [=](const SkSetupContext& ctx) { |
| sk_dilate_setup(ctx); |
| ctx.paint.setImageFilter(sk_dilate_filter_5); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_dilate_setup(ctx); |
| ctx.paint.setImageFilter(&dl_dilate_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()); |
| SkSetup sk_erode_setup = [=](const SkSetupContext& ctx) { |
| ctx.paint.setShader(MakeColorSource(ctx.image)); |
| ctx.paint.setStrokeWidth(6.0); |
| }; |
| DlSetup dl_erode_setup = [=](const DlSetupContext& ctx) { |
| ctx.paint.setColorSource(MakeColorSource(ctx.image)); |
| ctx.paint.setStrokeWidth(6.0); |
| }; |
| erode_env.init_ref(sk_erode_setup, testP.sk_renderer(), // |
| dl_erode_setup, testP.dl_renderer(), |
| testP.imp_renderer()); |
| quickCompareToReference(erode_env, "erode"); |
| // do not erode too much, because some tests assert there are enough |
| // pixels that are changed. |
| DlErodeImageFilter dl_erode_filter_1(1.0, 1.0); |
| auto sk_erode_filter_1 = SkImageFilters::Erode(1.0, 1.0, nullptr); |
| RenderWith(testP, erode_env, tolerance, |
| CaseParameters( |
| "ImageFilter == Erode 1", |
| [=](const SkSetupContext& ctx) { |
| sk_erode_setup(ctx); |
| ctx.paint.setImageFilter(sk_erode_filter_1); |
| }, |
| [=](const DlSetupContext& ctx) { |
| dl_erode_setup(ctx); |
| ctx.paint.setImageFilter(&dl_erode_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 dl_color_filter(rotate_color_matrix); |
| auto sk_color_filter = SkColorFilters::Matrix(rotate_color_matrix); |
| { |
| DlColor bg = DlColor::kWhite(); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "ColorFilter == RotateRGB", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setColor(SK_ColorYELLOW); |
| ctx.paint.setColorFilter(sk_color_filter); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setColor(DlColor::kYellow()); |
| ctx.paint.setColorFilter(&dl_color_filter); |
| }) |
| .with_bg(bg)); |
| } |
| { |
| DlColor bg = DlColor::kWhite(); |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "ColorFilter == Invert", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setColor(SK_ColorYELLOW); |
| ctx.paint.setColorFilter( |
| SkColorFilters::Matrix(invert_color_matrix)); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setColor(DlColor::kYellow()); |
| ctx.paint.setInvertColors(true); |
| }) |
| .with_bg(bg)); |
| } |
| } |
| |
| { |
| const DlBlurMaskFilter dl_mask_filter(DlBlurStyle::kNormal, 5.0); |
| auto sk_mask_filter = SkMaskFilter::MakeBlur(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", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setMaskFilter(sk_mask_filter); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setMaskFilter(&dl_mask_filter); |
| })); |
| } |
| } |
| |
| { |
| SkPoint end_points[] = { |
| SkPoint::Make(kRenderBounds.fLeft, kRenderBounds.fTop), |
| SkPoint::Make(kRenderBounds.fRight, kRenderBounds.fBottom), |
| }; |
| DlColor dl_colors[] = { |
| DlColor::kGreen(), |
| DlColor::kYellow().withAlpha(0x7f), |
| DlColor::kBlue(), |
| }; |
| SkColor sk_colors[] = { |
| SK_ColorGREEN, |
| SkColorSetA(SK_ColorYELLOW, 0x7f), |
| SK_ColorBLUE, |
| }; |
| float stops[] = { |
| 0.0, |
| 0.5, |
| 1.0, |
| }; |
| auto dl_gradient = |
| DlColorSource::MakeLinear(end_points[0], end_points[1], 3, dl_colors, |
| stops, DlTileMode::kMirror); |
| auto sk_gradient = SkGradientShader::MakeLinear( |
| end_points, sk_colors, stops, 3, SkTileMode::kMirror, 0, nullptr); |
| { |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "LinearGradient GYB", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setShader(sk_gradient); |
| ctx.paint.setDither(testP.uses_gradient()); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setColorSource(dl_gradient); |
| })); |
| } |
| } |
| } |
| |
| 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", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kFill_Style); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(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::kSoftwareBackend; |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Stroke + defaults", |
| [=](const SkSetupContext& ctx) { |
| if (no_hairlines) { |
| ctx.paint.setStrokeWidth(1.0); |
| } |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| }, |
| [=](const DlSetupContext& ctx) { |
| if (no_hairlines) { |
| ctx.paint.setStrokeWidth(1.0); |
| } |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| })); |
| |
| RenderWith(testP, env, tolerance, |
| CaseParameters( |
| "Fill + unnecessary StrokeWidth 10", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kFill_Style); |
| ctx.paint.setStrokeWidth(10.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kFill); |
| ctx.paint.setStrokeWidth(10.0); |
| })); |
| |
| RenderEnvironment stroke_base_env = |
| RenderEnvironment::MakeN32(env.provider()); |
| SkSetup sk_stroke_setup = [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| DlSetup dl_stroke_setup = [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| }; |
| stroke_base_env.init_ref(sk_stroke_setup, testP.sk_renderer(), |
| dl_stroke_setup, testP.dl_renderer(), |
| testP.imp_renderer()); |
| quickCompareToReference(stroke_base_env, "stroke"); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 10", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(10.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(10.0); |
| })); |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Square Cap", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeCap(SkPaint::kSquare_Cap); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeCap(DlStrokeCap::kSquare); |
| })); |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Round Cap", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeCap(SkPaint::kRound_Cap); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeCap(DlStrokeCap::kRound); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Bevel Join", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeJoin(SkPaint::kBevel_Join); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeJoin(DlStrokeJoin::kBevel); |
| })); |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Round Join", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeJoin(SkPaint::kRound_Join); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeJoin(DlStrokeJoin::kRound); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Miter 10", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeMiter(10.0); |
| ctx.paint.setStrokeJoin(SkPaint::kMiter_Join); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeMiter(10.0); |
| ctx.paint.setStrokeJoin(DlStrokeJoin::kMiter); |
| })); |
| |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "Stroke Width 5, Miter 0", |
| [=](const SkSetupContext& ctx) { |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeMiter(0.0); |
| ctx.paint.setStrokeJoin(SkPaint::kMiter_Join); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setStrokeMiter(0.0); |
| ctx.paint.setStrokeJoin(DlStrokeJoin::kMiter); |
| })); |
| |
| { |
| const SkScalar test_dashes_1[] = {29.0, 2.0}; |
| const SkScalar test_dashes_2[] = {17.0, 1.5}; |
| auto dl_dash_effect = DlDashPathEffect::Make(test_dashes_1, 2, 0.0f); |
| auto sk_dash_effect = SkDashPathEffect::Make(test_dashes_1, 2, 0.0f); |
| { |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "PathEffect without forced stroking == Dash-29-2", |
| [=](const SkSetupContext& ctx) { |
| // Provide some non-trivial stroke size to get dashed |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setPathEffect(sk_dash_effect); |
| }, |
| [=](const DlSetupContext& ctx) { |
| // Provide some non-trivial stroke size to get dashed |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setPathEffect(dl_dash_effect); |
| })); |
| } |
| { |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "PathEffect == Dash-29-2", |
| [=](const SkSetupContext& ctx) { |
| // Need stroke style to see dashing properly |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| // Provide some non-trivial stroke size to get dashed |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setPathEffect(sk_dash_effect); |
| }, |
| [=](const DlSetupContext& ctx) { |
| // Need stroke style to see dashing properly |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| // Provide some non-trivial stroke size to get dashed |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setPathEffect(dl_dash_effect); |
| })); |
| } |
| dl_dash_effect = DlDashPathEffect::Make(test_dashes_2, 2, 0.0f); |
| sk_dash_effect = SkDashPathEffect::Make(test_dashes_2, 2, 0.0f); |
| { |
| RenderWith(testP, stroke_base_env, tolerance, |
| CaseParameters( |
| "PathEffect == Dash-17-1.5", |
| [=](const SkSetupContext& ctx) { |
| // Need stroke style to see dashing properly |
| ctx.paint.setStyle(SkPaint::kStroke_Style); |
| // Provide some non-trivial stroke size to get dashed |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setPathEffect(sk_dash_effect); |
| }, |
| [=](const DlSetupContext& ctx) { |
| // Need stroke style to see dashing properly |
| ctx.paint.setDrawStyle(DlDrawStyle::kStroke); |
| // Provide some non-trivial stroke size to get dashed |
| ctx.paint.setStrokeWidth(5.0); |
| ctx.paint.setPathEffect(dl_dash_effect); |
| })); |
| } |
| } |
| } |
| |
| 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", // |
| [=](const SkSetupContext& ctx) { ctx.canvas->translate(5, 10); }, |
| [=](const DlSetupContext& ctx) { ctx.canvas->Translate(5, 10); })); |
| RenderWith( // |
| testP, env, tolerance, |
| CaseParameters( |
| "Scale +5%", // |
| [=](const SkSetupContext& ctx) { ctx.canvas->scale(1.05, 1.05); }, |
| [=](const DlSetupContext& ctx) { ctx.canvas->Scale(1.05, 1.05); })); |
| RenderWith( // |
| testP, env, skewed_tolerance, |
| CaseParameters( |
| "Rotate 5 degrees", // |
| [=](const SkSetupContext& ctx) { ctx.canvas->rotate(5); }, |
| [=](const DlSetupContext& ctx) { ctx.canvas->Rotate(5); })); |
| RenderWith( // |
| testP, env, skewed_tolerance, |
| CaseParameters( |
| "Skew 5%", // |
| [=](const SkSetupContext& ctx) { ctx.canvas->skew(0.05, 0.05); }, |
| [=](const DlSetupContext& ctx) { ctx.canvas->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", |
| [=](const SkSetupContext& ctx) { ctx.canvas->concat(tx); }, |
| [=](const DlSetupContext& ctx) { ctx.canvas->Transform(tx); })); |
| } |
| { |
| 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", |
| [=](const SkSetupContext& ctx) { ctx.canvas->concat(m44); }, |
| [=](const DlSetupContext& ctx) { ctx.canvas->Transform(m44); })); |
| } |
| } |
| |
| 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", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipRect(r_clip, SkClipOp::kIntersect, false); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipRect(r_clip, ClipOp::kIntersect, false); |
| })); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "AntiAlias ClipRect inset by 15.4", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipRect(r_clip, SkClipOp::kIntersect, true); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipRect(r_clip, ClipOp::kIntersect, true); |
| })); |
| RenderWith(testP, env, diff_tolerance, |
| CaseParameters( |
| "Hard ClipRect Diff, inset by 15.4", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipRect(r_clip, SkClipOp::kDifference, false); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->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", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipRRect(rr_clip, SkClipOp::kIntersect, |
| false); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipRRect(rr_clip, ClipOp::kIntersect, false); |
| })); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "AntiAlias ClipRRect with radius of 15.4", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipRRect(rr_clip, SkClipOp::kIntersect, true); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipRRect(rr_clip, ClipOp::kIntersect, true); |
| })); |
| RenderWith(testP, env, diff_tolerance, |
| CaseParameters( |
| "Hard ClipRRect Diff, with radius of 15.4", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipRRect(rr_clip, SkClipOp::kDifference, |
| false); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->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", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipPath(path_clip, SkClipOp::kIntersect, |
| false); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipPath(path_clip, ClipOp::kIntersect, false); |
| })); |
| RenderWith(testP, env, intersect_tolerance, |
| CaseParameters( |
| "AntiAlias ClipPath inset by 15.4", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipPath(path_clip, SkClipOp::kIntersect, |
| true); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipPath(path_clip, ClipOp::kIntersect, true); |
| })); |
| RenderWith( |
| testP, env, diff_tolerance, |
| CaseParameters( |
| "Hard ClipPath Diff, inset by 15.4", |
| [=](const SkSetupContext& ctx) { |
| ctx.canvas->clipPath(path_clip, SkClipOp::kDifference, false); |
| }, |
| [=](const DlSetupContext& ctx) { |
| ctx.canvas->ClipPath(path_clip, ClipOp::kDifference, false); |
| }) |
| .with_diff_clip()); |
| } |
| |
| enum class DirectoryStatus { |
| kExisted, |
| kCreated, |
| kFailed, |
| }; |
| |
| static DirectoryStatus CheckDir(const std::string& dir) { |
| auto ret = |
| fml::OpenDirectory(dir.c_str(), false, fml::FilePermission::kRead); |
| if (ret.is_valid()) { |
| return DirectoryStatus::kExisted; |
| } |
| ret = |
| fml::OpenDirectory(dir.c_str(), true, fml::FilePermission::kReadWrite); |
| if (ret.is_valid()) { |
| return DirectoryStatus::kCreated; |
| } |
| FML_LOG(ERROR) << "Could not create directory (" << dir |
| << ") for impeller failure images" << ", ret = " << ret.get() |
| << ", errno = " << errno; |
| return DirectoryStatus::kFailed; |
| } |
| |
| static void SetupImpellerFailureImageDirectory() { |
| std::string base_dir = "./impeller_failure_images"; |
| if (CheckDir(base_dir) == DirectoryStatus::kFailed) { |
| return; |
| } |
| for (int i = 0; i < 10000; i++) { |
| std::string sub_dir = std::to_string(i); |
| while (sub_dir.length() < 4) { |
| sub_dir = "0" + sub_dir; |
| } |
| std::string try_dir = base_dir + "/" + sub_dir; |
| switch (CheckDir(try_dir)) { |
| case DirectoryStatus::kExisted: |
| break; |
| case DirectoryStatus::kCreated: |
| ImpellerFailureImageDirectory = try_dir; |
| return; |
| case DirectoryStatus::kFailed: |
| return; |
| } |
| } |
| FML_LOG(ERROR) << "Too many output directories for Impeller failure images"; |
| } |
| |
| static void save_to_png(const RenderResult* result, |
| const std::string& op_desc, |
| const std::string& reason) { |
| if (!SaveImpellerFailureImages) { |
| return; |
| } |
| if (ImpellerFailureImageDirectory.length() == 0) { |
| SetupImpellerFailureImageDirectory(); |
| if (ImpellerFailureImageDirectory.length() == 0) { |
| SaveImpellerFailureImages = false; |
| return; |
| } |
| } |
| |
| std::string filename = ImpellerFailureImageDirectory + "/"; |
| for (const char& ch : op_desc) { |
| filename += (ch == ':' || ch == ' ') ? '_' : ch; |
| } |
| filename = filename + ".png"; |
| result->write(filename); |
| ImpellerFailureImages.push_back(filename); |
| FML_LOG(ERROR) << reason << ": " << filename; |
| } |
| |
| static void RenderWith(const TestParameters& testP, |
| const RenderEnvironment& env, |
| const BoundsTolerance& tolerance_in, |
| const CaseParameters& caseP) { |
| std::string test_name = |
| ::testing::UnitTest::GetInstance()->current_test_info()->name(); |
| const std::string info = |
| env.backend_name() + ": " + test_name + " (" + caseP.info() + ")"; |
| const DlColor bg = caseP.bg(); |
| RenderJobInfo base_info = { |
| .bg = bg, |
| }; |
| |
| // sk_result is a direct rendering via SkCanvas to SkSurface |
| // DisplayList mechanisms are not involved in this operation |
| SkJobRenderer sk_job(caseP.sk_setup(), // |
| testP.sk_renderer(), // |
| caseP.sk_restore(), // |
| env.sk_image()); |
| auto sk_result = env.getResult(base_info, sk_job); |
| |
| DlJobRenderer dl_job(caseP.dl_setup(), // |
| testP.dl_renderer(), // |
| caseP.dl_restore(), // |
| env.dl_image()); |
| auto dl_result = env.getResult(base_info, dl_job); |
| |
| EXPECT_EQ(sk_job.setup_matrix(), dl_job.setup_matrix()); |
| EXPECT_EQ(sk_job.setup_clip_bounds(), dl_job.setup_clip_bounds()); |
| ASSERT_EQ(sk_result->width(), kTestWidth) << info; |
| ASSERT_EQ(sk_result->height(), kTestHeight) << info; |
| ASSERT_EQ(dl_result->width(), kTestWidth) << info; |
| ASSERT_EQ(dl_result->height(), kTestHeight) << info; |
| |
| const BoundsTolerance tolerance = |
| testP.adjust(tolerance_in, dl_job.setup_paint(), dl_job.setup_matrix()); |
| const sk_sp<SkPicture> sk_picture = sk_job.MakePicture(base_info); |
| const sk_sp<DisplayList> display_list = dl_job.MakeDisplayList(base_info); |
| |
| SkRect sk_bounds = sk_picture->cullRect(); |
| checkPixels(sk_result.get(), sk_bounds, info + " (Skia reference)", bg); |
| |
| if (testP.should_match(env, caseP, dl_job.setup_paint(), dl_job)) { |
| quickCompareToReference(env.ref_sk_result(), sk_result.get(), true, |
| info + " (attribute should not have effect)"); |
| } else { |
| quickCompareToReference(env.ref_sk_result(), sk_result.get(), false, |
| info + " (attribute should affect rendering)"); |
| } |
| |
| // If either the reference setup or the test setup contain attributes |
| // that Impeller doesn't support, we skip the Impeller testing. This |
| // is mostly stroked or patterned text which is vectored through drawPath |
| // for Impeller. |
| if (env.supports_impeller() && |
| testP.impeller_compatible(dl_job.setup_paint()) && |
| testP.impeller_compatible(env.ref_dl_paint())) { |
| DlJobRenderer imp_job(caseP.dl_setup(), // |
| testP.imp_renderer(), // |
| caseP.dl_restore(), // |
| env.impeller_image()); |
| auto imp_result = env.getImpellerResult(base_info, imp_job); |
| std::string imp_info = info + " (Impeller)"; |
| bool success = checkPixels(imp_result.get(), imp_result->render_bounds(), |
| imp_info, bg); |
| if (testP.should_match(env, caseP, imp_job.setup_paint(), imp_job)) { |
| success = success && // |
| quickCompareToReference( // |
| env.ref_impeller_result(), imp_result.get(), true, |
| imp_info + " (attribute should not have effect)"); |
| } else { |
| success = success && // |
| quickCompareToReference( // |
| env.ref_impeller_result(), imp_result.get(), false, |
| imp_info + " (attribute should affect rendering)"); |
| } |
| if (SaveImpellerFailureImages && !success) { |
| FML_LOG(ERROR) << "Impeller issue encountered for: " |
| << *imp_job.MakeDisplayList(base_info); |
| save_to_png(imp_result.get(), info + " (Impeller Result)", |
| "output saved in"); |
| save_to_png(env.ref_impeller_result(), info + " (Impeller Reference)", |
| "compare to reference without attributes"); |
| save_to_png(sk_result.get(), info + " (Skia Result)", |
| "and to Skia reference with attributes"); |
| save_to_png(env.ref_sk_result(), info + " (Skia Reference)", |
| "and to Skia reference without attributes"); |
| } |
| } |
| |
| quickCompareToReference(sk_result.get(), dl_result.get(), true, |
| info + " (DlCanvas output matches SkCanvas)"); |
| |
| { |
| 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 (!dl_bounds.isEmpty() && |
| !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; |
| EXPECT_EQ(static_cast<int>(display_list->op_count()), |
| sk_picture->approximateOpCount()) |
| << info; |
| } |
| |
| DisplayListJobRenderer dl_builder_job(display_list); |
| auto dl_builder_result = env.getResult(base_info, dl_builder_job); |
| if (caseP.fuzzy_compare_components()) { |
| compareToReference( |
| dl_builder_result.get(), dl_result.get(), |
| info + " (DlCanvas DL output close to Builder Dl output)", |
| &dl_bounds, &tolerance, bg, true); |
| } else { |
| quickCompareToReference( |
| dl_builder_result.get(), dl_result.get(), true, |
| info + " (DlCanvas DL output matches Builder Dl output)"); |
| } |
| |
| 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 sequence uses an SkPicture generated previously from the SkCanvas |
| // calls and a DisplayList generated previously from the DlCanvas calls |
| // and 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; |
| |
| SkPictureJobRenderer 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; |
| |
| DisplayListJobRenderer 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::k565PixelFormat) { |
| return 9; |
| } |
| if (env.provider()->backend_type() == BackendType::kOpenGlBackend) { |
| // 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; |
| |
| DisplayListJobRenderer 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 bool checkPixels(const RenderResult* ref_result, |
| const SkRect ref_bounds, |
| const std::string& info, |
| const DlColor bg = DlColor::kTransparent()) { |
| uint32_t untouched = bg.premultipliedArgb(); |
| int pixels_touched = 0; |
| int pixels_oob = 0; |
| SkIRect i_bounds = ref_bounds.roundOut(); |
| EXPECT_EQ(ref_result->width(), kTestWidth) << info; |
| EXPECT_EQ(ref_result->height(), kTestWidth) << info; |
| 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++; |
| } |
| } |
| } |
| } |
| EXPECT_EQ(pixels_oob, 0) << info; |
| EXPECT_GT(pixels_touched, 0) << info; |
| return pixels_oob == 0 && pixels_touched > 0; |
| } |
| |
| static int countModifiedTransparentPixels(const RenderResult* ref_result, |
| const RenderResult* test_result) { |
| int count = 0; |
| for (int y = 0; y < kTestHeight; 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 < kTestWidth; x++) { |
| if (ref_row[x] != test_row[x]) { |
| if (ref_row[x] == 0) { |
| count++; |
| } |
| } |
| } |
| } |
| return count; |
| } |
| |
| static void quickCompareToReference(const RenderEnvironment& env, |
| const std::string& info) { |
| quickCompareToReference(env.ref_sk_result(), env.ref_dl_result(), true, |
| info + " reference rendering"); |
| } |
| |
|