blob: 91023c00b34c67611cd4939602c72e72b55437d3 [file] [log] [blame]
// 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");
}