fix the bounds of nested save layers with a mix of clips and transforms (flutter/engine#31437)
This commit is contained in:
@@ -179,7 +179,8 @@ static constexpr SkIRect TestLatticeSrcRect = {1, 1, 39, 39};
|
||||
|
||||
static sk_sp<SkPicture> MakeTestPicture(int w, int h, SkColor color) {
|
||||
SkPictureRecorder recorder;
|
||||
SkCanvas* cv = recorder.beginRecording(TestBounds);
|
||||
SkRTreeFactory rtree_factory;
|
||||
SkCanvas* cv = recorder.beginRecording(TestBounds, &rtree_factory);
|
||||
SkPaint paint;
|
||||
paint.setColor(color);
|
||||
paint.setStyle(SkPaint::kFill_Style);
|
||||
@@ -1030,7 +1031,8 @@ TEST(DisplayList, DisplayListSaveLayerBoundsWithAlphaFilter) {
|
||||
// that modifies alpha channels (save layer bounds are meaningless
|
||||
// under those circumstances)
|
||||
SkPictureRecorder recorder;
|
||||
SkCanvas* canvas = recorder.beginRecording(build_bounds);
|
||||
SkRTreeFactory rtree_factory;
|
||||
SkCanvas* canvas = recorder.beginRecording(build_bounds, &rtree_factory);
|
||||
SkPaint p1;
|
||||
p1.setColorFilter(alpha_color_filter);
|
||||
canvas->saveLayer(save_bounds, &p1);
|
||||
@@ -1826,5 +1828,128 @@ TEST(DisplayList, SaveLayerSrcBlendOnChildPreventsOpacityOptimization) {
|
||||
EXPECT_EQ(expector.save_layer_count(), 1);
|
||||
}
|
||||
|
||||
TEST(DisplayList, FlutterSvgIssue661BoundsWereEmpty) {
|
||||
// See https://github.com/dnfield/flutter_svg/issues/661
|
||||
|
||||
SkPath path1;
|
||||
path1.setFillType(SkPathFillType::kWinding);
|
||||
path1.moveTo(25.54f, 37.52f);
|
||||
path1.cubicTo(20.91f, 37.52f, 16.54f, 33.39f, 13.62f, 30.58f);
|
||||
path1.lineTo(13, 30);
|
||||
path1.lineTo(12.45f, 29.42f);
|
||||
path1.cubicTo(8.39f, 25.15f, 1.61f, 18, 8.37f, 11.27f);
|
||||
path1.cubicTo(10.18f, 9.46f, 12.37f, 9.58f, 14.49f, 11.58f);
|
||||
path1.cubicTo(15.67f, 12.71f, 17.05f, 14.69f, 17.07f, 16.58f);
|
||||
path1.cubicTo(17.0968f, 17.458f, 16.7603f, 18.3081f, 16.14f, 18.93f);
|
||||
path1.cubicTo(15.8168f, 19.239f, 15.4653f, 19.5169f, 15.09f, 19.76f);
|
||||
path1.cubicTo(14.27f, 20.33f, 14.21f, 20.44f, 14.27f, 20.62f);
|
||||
path1.cubicTo(15.1672f, 22.3493f, 16.3239f, 23.9309f, 17.7f, 25.31f);
|
||||
path1.cubicTo(19.0791f, 26.6861f, 20.6607f, 27.8428f, 22.39f, 28.74f);
|
||||
path1.cubicTo(22.57f, 28.8f, 22.69f, 28.74f, 23.25f, 27.92f);
|
||||
path1.cubicTo(23.5f, 27.566f, 23.778f, 27.231f, 24.08f, 26.92f);
|
||||
path1.cubicTo(24.7045f, 26.3048f, 25.5538f, 25.9723f, 26.43f, 26);
|
||||
path1.cubicTo(28.29f, 26, 30.27f, 27.4f, 31.43f, 28.58f);
|
||||
path1.cubicTo(33.43f, 30.67f, 33.55f, 32.9f, 31.74f, 34.7f);
|
||||
path1.cubicTo(30.1477f, 36.4508f, 27.906f, 37.4704f, 25.54f, 37.52f);
|
||||
path1.close();
|
||||
path1.moveTo(11.17f, 12.23f);
|
||||
path1.cubicTo(10.6946f, 12.2571f, 10.2522f, 12.4819f, 9.95f, 12.85f);
|
||||
path1.cubicTo(5.12f, 17.67f, 8.95f, 22.5f, 14.05f, 27.85f);
|
||||
path1.lineTo(14.62f, 28.45f);
|
||||
path1.lineTo(15.16f, 28.96f);
|
||||
path1.cubicTo(20.52f, 34.06f, 25.35f, 37.89f, 30.16f, 33.06f);
|
||||
path1.cubicTo(30.83f, 32.39f, 31.25f, 31.56f, 29.81f, 30.06f);
|
||||
path1.cubicTo(28.9247f, 29.07f, 27.7359f, 28.4018f, 26.43f, 28.16f);
|
||||
path1.cubicTo(26.1476f, 28.1284f, 25.8676f, 28.2367f, 25.68f, 28.45f);
|
||||
path1.cubicTo(25.4633f, 28.6774f, 25.269f, 28.9252f, 25.1f, 29.19f);
|
||||
path1.cubicTo(24.53f, 30.01f, 23.47f, 31.54f, 21.54f, 30.79f);
|
||||
path1.lineTo(21.41f, 30.72f);
|
||||
path1.cubicTo(19.4601f, 29.7156f, 17.6787f, 28.4133f, 16.13f, 26.86f);
|
||||
path1.cubicTo(14.5748f, 25.3106f, 13.2693f, 23.5295f, 12.26f, 21.58f);
|
||||
path1.lineTo(12.2f, 21.44f);
|
||||
path1.cubicTo(11.45f, 19.51f, 12.97f, 18.44f, 13.8f, 17.88f);
|
||||
path1.cubicTo(14.061f, 17.706f, 14.308f, 17.512f, 14.54f, 17.3f);
|
||||
path1.cubicTo(14.7379f, 17.1067f, 14.8404f, 16.8359f, 14.82f, 16.56f);
|
||||
path1.cubicTo(14.5978f, 15.268f, 13.9585f, 14.0843f, 13, 13.19f);
|
||||
path1.cubicTo(12.5398f, 12.642f, 11.8824f, 12.2971f, 11.17f, 12.23f);
|
||||
path1.lineTo(11.17f, 12.23f);
|
||||
path1.close();
|
||||
path1.moveTo(27, 19.34f);
|
||||
path1.lineTo(24.74f, 19.34f);
|
||||
path1.cubicTo(24.7319f, 18.758f, 24.262f, 18.2881f, 23.68f, 18.28f);
|
||||
path1.lineTo(23.68f, 16.05f);
|
||||
path1.lineTo(23.7f, 16.05f);
|
||||
path1.cubicTo(25.5153f, 16.0582f, 26.9863f, 17.5248f, 27, 19.34f);
|
||||
path1.lineTo(27, 19.34f);
|
||||
path1.close();
|
||||
path1.moveTo(32.3f, 19.34f);
|
||||
path1.lineTo(30.07f, 19.34f);
|
||||
path1.cubicTo(30.037f, 15.859f, 27.171f, 13.011f, 23.69f, 13);
|
||||
path1.lineTo(23.69f, 10.72f);
|
||||
path1.cubicTo(28.415f, 10.725f, 32.3f, 14.615f, 32.3f, 19.34f);
|
||||
path1.close();
|
||||
|
||||
SkPath path2;
|
||||
path2.setFillType(SkPathFillType::kWinding);
|
||||
path2.moveTo(37.5f, 19.33f);
|
||||
path2.lineTo(35.27f, 19.33f);
|
||||
path2.cubicTo(35.265f, 12.979f, 30.041f, 7.755f, 23.69f, 7.75f);
|
||||
path2.lineTo(23.69f, 5.52f);
|
||||
path2.cubicTo(31.264f, 5.525f, 37.495f, 11.756f, 37.5f, 19.33f);
|
||||
path2.close();
|
||||
|
||||
DisplayListBuilder builder;
|
||||
{
|
||||
builder.save();
|
||||
builder.clipRect({0, 0, 100, 100}, SkClipOp::kIntersect, true);
|
||||
{
|
||||
builder.save();
|
||||
builder.transform2DAffine(2.17391, 0, -2547.83, //
|
||||
0, 2.04082, -500);
|
||||
{
|
||||
builder.save();
|
||||
builder.clipRect({1172, 245, 1218, 294}, SkClipOp::kIntersect, true);
|
||||
{
|
||||
builder.saveLayer(nullptr, SaveLayerOptions::kWithAttributes);
|
||||
{
|
||||
builder.save();
|
||||
builder.transform2DAffine(1.4375, 0, 1164.09, //
|
||||
0, 1.53125, 236.548);
|
||||
builder.setAntiAlias(1);
|
||||
builder.setColor(0xffffffff);
|
||||
builder.drawPath(path1);
|
||||
builder.restore();
|
||||
}
|
||||
{
|
||||
builder.save();
|
||||
builder.transform2DAffine(1.4375, 0, 1164.09, //
|
||||
0, 1.53125, 236.548);
|
||||
builder.drawPath(path2);
|
||||
builder.restore();
|
||||
}
|
||||
builder.restore();
|
||||
}
|
||||
builder.restore();
|
||||
}
|
||||
builder.restore();
|
||||
}
|
||||
builder.restore();
|
||||
}
|
||||
sk_sp<DisplayList> display_list = builder.Build();
|
||||
// Prior to the fix, the bounds were empty.
|
||||
EXPECT_FALSE(display_list->bounds().isEmpty());
|
||||
// These are the expected bounds, but testing float values can be
|
||||
// flaky wrt minor changes in the bounds calculations. If this
|
||||
// line has to be revised too often as the DL implementation is
|
||||
// improved and maintained, then we can eliminate this test and
|
||||
// just rely on the "rounded out" bounds test that follows.
|
||||
EXPECT_EQ(display_list->bounds(),
|
||||
SkRect::MakeLTRB(0, 0.00189208984375, 99.9839630127, 100));
|
||||
// This is the more practical result. The bounds are "almost" 0,0,100x100
|
||||
EXPECT_EQ(display_list->bounds().roundOut(), SkIRect::MakeWH(100, 100));
|
||||
EXPECT_EQ(display_list->op_count(), 19u);
|
||||
EXPECT_EQ(display_list->bytes(), sizeof(DisplayList) + 304u);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace flutter
|
||||
|
||||
@@ -237,7 +237,7 @@ void ClipBoundsDispatchHelper::reset(const SkRect* cull_rect) {
|
||||
DisplayListBoundsCalculator::DisplayListBoundsCalculator(
|
||||
const SkRect* cull_rect)
|
||||
: ClipBoundsDispatchHelper(cull_rect) {
|
||||
layer_infos_.emplace_back(std::make_unique<RootLayerData>());
|
||||
layer_infos_.emplace_back(std::make_unique<LayerData>(nullptr));
|
||||
accumulator_ = layer_infos_.back()->layer_accumulator();
|
||||
}
|
||||
void DisplayListBoundsCalculator::setStrokeCap(SkPaint::Cap cap) {
|
||||
@@ -284,7 +284,7 @@ void DisplayListBoundsCalculator::setMaskBlurFilter(SkBlurStyle style,
|
||||
void DisplayListBoundsCalculator::save() {
|
||||
SkMatrixDispatchHelper::save();
|
||||
ClipBoundsDispatchHelper::save();
|
||||
layer_infos_.emplace_back(std::make_unique<SaveData>(accumulator_));
|
||||
layer_infos_.emplace_back(std::make_unique<LayerData>(accumulator_));
|
||||
accumulator_ = layer_infos_.back()->layer_accumulator();
|
||||
}
|
||||
void DisplayListBoundsCalculator::saveLayer(const SkRect* bounds,
|
||||
@@ -292,16 +292,27 @@ void DisplayListBoundsCalculator::saveLayer(const SkRect* bounds,
|
||||
SkMatrixDispatchHelper::save();
|
||||
ClipBoundsDispatchHelper::save();
|
||||
if (options.renders_with_attributes()) {
|
||||
layer_infos_.emplace_back(std::make_unique<SaveLayerData>(
|
||||
accumulator_, image_filter_, paint_nops_on_transparency()));
|
||||
// The actual flood of the outer layer clip will occur after the
|
||||
// (eventual) corresponding restore is called, but rather than
|
||||
// remember this information in the LayerInfo until the restore
|
||||
// method is processed, we just mark the unbounded state up front.
|
||||
if (!paint_nops_on_transparency()) {
|
||||
// We will fill the clip of the outer layer when we restore
|
||||
AccumulateUnbounded();
|
||||
}
|
||||
|
||||
layer_infos_.emplace_back(
|
||||
std::make_unique<LayerData>(accumulator_, image_filter_));
|
||||
} else {
|
||||
layer_infos_.emplace_back(
|
||||
std::make_unique<SaveLayerData>(accumulator_, nullptr, true));
|
||||
std::make_unique<LayerData>(accumulator_, nullptr));
|
||||
}
|
||||
|
||||
accumulator_ = layer_infos_.back()->layer_accumulator();
|
||||
// Accumulate the layer in its own coordinate system and then
|
||||
// filter and transform its bounds on restore.
|
||||
SkMatrixDispatchHelper::reset();
|
||||
|
||||
// Even though Skia claims that the bounds are only a hint, they actually
|
||||
// use them as the temporary layer bounds during rendering the layer, so
|
||||
// we set them as if a clip operation were performed.
|
||||
if (bounds) {
|
||||
clipRect(*bounds, SkClipOp::kIntersect, false);
|
||||
}
|
||||
@@ -310,24 +321,53 @@ void DisplayListBoundsCalculator::restore() {
|
||||
if (layer_infos_.size() > 1) {
|
||||
SkMatrixDispatchHelper::restore();
|
||||
ClipBoundsDispatchHelper::restore();
|
||||
accumulator_ = layer_infos_.back()->restore_accumulator();
|
||||
SkRect layer_bounds = layer_infos_.back()->layer_bounds();
|
||||
// Must read unbounded state after layer_bounds
|
||||
bool layer_unbounded = layer_infos_.back()->is_unbounded();
|
||||
|
||||
// Remember a few pieces of information from the current layer info
|
||||
// for later processing.
|
||||
LayerData* layer_info = layer_infos_.back().get();
|
||||
BoundsAccumulator* outer_accumulator = layer_info->restore_accumulator();
|
||||
bool is_unbounded = layer_info->is_unbounded();
|
||||
|
||||
// Before we pop_back we will get the current layer bounds from the
|
||||
// current accumulator and adjust ot as required based on the filter.
|
||||
SkRect layer_bounds = accumulator_->bounds();
|
||||
sk_sp<SkImageFilter> filter = layer_info->filter();
|
||||
if (filter) {
|
||||
if (filter->canComputeFastBounds()) {
|
||||
SkIRect filter_bounds =
|
||||
filter->filterBounds(layer_bounds.roundOut(), matrix(),
|
||||
SkImageFilter::kForward_MapDirection);
|
||||
layer_bounds.set(filter_bounds);
|
||||
|
||||
// We could leave the clipping to the code below that will
|
||||
// finally accumulate the layer bounds, but the bounds do
|
||||
// not normally need clipping unless they were modified by
|
||||
// entering this filtering code path.
|
||||
if (has_clip() && !layer_bounds.intersect(clip_bounds())) {
|
||||
layer_bounds.setEmpty();
|
||||
}
|
||||
} else {
|
||||
// If the filter cannot compute bounds then it might take an
|
||||
// unbounded amount of space. This can sometimes happen if it
|
||||
// modifies transparent black which means its affect will not
|
||||
// be bounded by the transparent pixels outside of the layer
|
||||
// drawable.
|
||||
is_unbounded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the accumulator before popping the LayerInfo so that
|
||||
// it nevers points to an out of scope instance.
|
||||
accumulator_ = outer_accumulator;
|
||||
layer_infos_.pop_back();
|
||||
|
||||
// We accumulate the bounds even if the layer was unbounded because
|
||||
// the unbounded state may be contained at a higher level, so we at
|
||||
// least accumulate our best estimate about what we have.
|
||||
if (!layer_bounds.isEmpty()) {
|
||||
// We do not use AccumulateOpBounds because the layer info already
|
||||
// applied all bounds modifications based on the attributes that
|
||||
// were in place when it was created. Modifying the bounds further
|
||||
// based on the current attributes would mix attribute states.
|
||||
// The bounds are still transformed and clipped by this method.
|
||||
AccumulateBounds(layer_bounds);
|
||||
}
|
||||
if (layer_unbounded) {
|
||||
// Finally accumulate the impact of the layer into the new scope.
|
||||
// Note that the bounds were already accumulated in device pixels
|
||||
// and clipped to any clips involved so we do not need to go
|
||||
// through any transforms or clips to accuulate them into this
|
||||
// layer.
|
||||
accumulator_->accumulate(layer_bounds);
|
||||
if (is_unbounded) {
|
||||
AccumulateUnbounded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ class BoundsAccumulator {
|
||||
}
|
||||
}
|
||||
void accumulate(const SkRect& r) {
|
||||
if (r.fLeft <= r.fRight && r.fTop <= r.fBottom) {
|
||||
if (r.fLeft < r.fRight && r.fTop < r.fBottom) {
|
||||
accumulate(r.fLeft, r.fTop);
|
||||
accumulate(r.fRight, r.fBottom);
|
||||
}
|
||||
@@ -495,41 +495,37 @@ class DisplayListBoundsCalculator final
|
||||
// current accumulator based on saveLayer history
|
||||
BoundsAccumulator* accumulator_;
|
||||
|
||||
// A class that abstracts the information kept for a single
|
||||
// |save| or |saveLayer|, including the root information that
|
||||
// is kept as a base set of information for the DisplayList
|
||||
// at the initial conditions outside of any saveLayer.
|
||||
// See |RootLayerData|, |SaveData| and |SaveLayerData|.
|
||||
// A class that remembers the information kept for a single
|
||||
// |save| or |saveLayer|.
|
||||
// Each save or saveLayer will maintain its own bounds accumulator
|
||||
// and then accumulate that back into the surrounding accumulator
|
||||
// during restore.
|
||||
class LayerData {
|
||||
public:
|
||||
// Construct a LayerData to push on the save stack for a |save|
|
||||
// or |saveLayer| call.
|
||||
// There does not tend to be an actual layer in the case of
|
||||
// a |save| call, but in order to homogenize the handling
|
||||
// of |restore| it adds a trivial implementation of this
|
||||
// class to the stack of saves.
|
||||
// The |outer| parameter is the |BoundsAccumulator| that was
|
||||
// in use by the stream before this layer was pushed on the
|
||||
// stack and should be returned when this layer is popped off
|
||||
// the stack.
|
||||
// Some layers may substitute their own accumulator to compute
|
||||
// their own local bounds while they are on the stack.
|
||||
explicit LayerData(BoundsAccumulator* outer)
|
||||
: outer_(outer), is_unbounded_(false) {}
|
||||
virtual ~LayerData() = default;
|
||||
// Some saveLayer calls will process their bounds by an
|
||||
// |SkImageFilter| when they are restored, but for most
|
||||
// saveLayer (and all save) calls the filter will be null.
|
||||
explicit LayerData(BoundsAccumulator* outer,
|
||||
sk_sp<SkImageFilter> filter = nullptr)
|
||||
: outer_(outer), filter_(filter), is_unbounded_(false) {}
|
||||
~LayerData() = default;
|
||||
|
||||
// The accumulator to use while this layer is put in play by
|
||||
// a |save| or |saveLayer|
|
||||
virtual BoundsAccumulator* layer_accumulator() { return outer_; }
|
||||
BoundsAccumulator* layer_accumulator() { return &layer_accumulator_; }
|
||||
|
||||
// The accumulator to use after this layer is removed from play
|
||||
// via |restore|
|
||||
virtual BoundsAccumulator* restore_accumulator() { return outer_; }
|
||||
BoundsAccumulator* restore_accumulator() { return outer_; }
|
||||
|
||||
// The bounds of this layer. May be empty for cases like
|
||||
// a non-layer |save| call which uses the |outer_| accumulator
|
||||
// to accumulate draw calls inside of it
|
||||
virtual SkRect layer_bounds() = 0;
|
||||
// The filter to apply to the layer bounds when it is restored
|
||||
sk_sp<SkImageFilter> filter() { return filter_; }
|
||||
|
||||
// is_unbounded should be set to true if we ever encounter an operation
|
||||
// on a layer that either is unrestricted (|drawColor| or |drawPaint|)
|
||||
@@ -562,86 +558,14 @@ class DisplayListBoundsCalculator final
|
||||
bool is_unbounded() const { return is_unbounded_; }
|
||||
|
||||
private:
|
||||
BoundsAccumulator layer_accumulator_;
|
||||
BoundsAccumulator* outer_;
|
||||
sk_sp<SkImageFilter> filter_;
|
||||
bool is_unbounded_;
|
||||
|
||||
FML_DISALLOW_COPY_AND_ASSIGN(LayerData);
|
||||
};
|
||||
|
||||
// An intermediate implementation class that handles keeping
|
||||
// a local accumulator for the layer, used by both |RootLayerData|
|
||||
// and |SaveLayerData|.
|
||||
class AccumulatorLayerData : public LayerData {
|
||||
public:
|
||||
BoundsAccumulator* layer_accumulator() override {
|
||||
return &layer_accumulator_;
|
||||
}
|
||||
|
||||
SkRect layer_bounds() override {
|
||||
// Even though this layer might be unbounded, we still
|
||||
// accumulate what bounds we have as the unbounded condition
|
||||
// may be contained at a higher level and we at least want to
|
||||
// account for the bounds that we do have.
|
||||
return layer_accumulator_.bounds();
|
||||
}
|
||||
|
||||
protected:
|
||||
using LayerData::LayerData;
|
||||
~AccumulatorLayerData() = default;
|
||||
|
||||
private:
|
||||
BoundsAccumulator layer_accumulator_;
|
||||
};
|
||||
|
||||
// Used as the initial default layer info for the Calculator.
|
||||
class RootLayerData final : public AccumulatorLayerData {
|
||||
public:
|
||||
RootLayerData() : AccumulatorLayerData(nullptr) {}
|
||||
~RootLayerData() = default;
|
||||
|
||||
private:
|
||||
FML_DISALLOW_COPY_AND_ASSIGN(RootLayerData);
|
||||
};
|
||||
|
||||
// Used for |save|
|
||||
class SaveData final : public LayerData {
|
||||
public:
|
||||
using LayerData::LayerData;
|
||||
~SaveData() = default;
|
||||
|
||||
SkRect layer_bounds() override { return SkRect::MakeEmpty(); }
|
||||
|
||||
private:
|
||||
FML_DISALLOW_COPY_AND_ASSIGN(SaveData);
|
||||
};
|
||||
|
||||
// Used for |saveLayer|
|
||||
class SaveLayerData final : public AccumulatorLayerData {
|
||||
public:
|
||||
SaveLayerData(BoundsAccumulator* outer,
|
||||
sk_sp<SkImageFilter> filter,
|
||||
bool paint_nops_on_transparency)
|
||||
: AccumulatorLayerData(outer), layer_filter_(std::move(filter)) {
|
||||
if (!paint_nops_on_transparency) {
|
||||
set_unbounded();
|
||||
}
|
||||
}
|
||||
~SaveLayerData() = default;
|
||||
|
||||
SkRect layer_bounds() override {
|
||||
SkRect bounds = AccumulatorLayerData::layer_bounds();
|
||||
if (!ComputeFilteredBounds(bounds, layer_filter_.get())) {
|
||||
set_unbounded();
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private:
|
||||
sk_sp<SkImageFilter> layer_filter_;
|
||||
|
||||
FML_DISALLOW_COPY_AND_ASSIGN(SaveLayerData);
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<LayerData>> layer_infos_;
|
||||
|
||||
static constexpr SkScalar kMinStrokeWidth = 0.01;
|
||||
|
||||
Reference in New Issue
Block a user