From 46a8def32637c8e2a65baa4e5f9eb16fdef38cc6 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Tue, 14 Dec 2021 16:19:09 -0800 Subject: [PATCH] Opacity peephole optimization (flutter/engine#29775) --- engine/src/flutter/common/graphics/texture.h | 3 +- engine/src/flutter/flow/display_list.cc | 93 +++++++-- engine/src/flutter/flow/display_list.h | 121 ++++++++++-- .../src/flutter/flow/display_list_canvas.cc | 45 +++-- engine/src/flutter/flow/display_list_canvas.h | 7 +- .../flow/display_list_canvas_unittests.cc | 135 ++++++++++--- .../flutter/flow/display_list_unittests.cc | 181 ++++++++++++++++++ engine/src/flutter/flow/display_list_utils.cc | 23 +++ engine/src/flutter/flow/display_list_utils.h | 25 +++ .../flutter/flow/layers/clip_path_layer.cc | 21 +- .../flutter/flow/layers/clip_rect_layer.cc | 21 +- .../flutter/flow/layers/clip_rrect_layer.cc | 21 +- .../flutter/flow/layers/container_layer.cc | 29 +++ .../flutter/flow/layers/display_list_layer.cc | 27 ++- engine/src/flutter/flow/layers/layer.cc | 3 +- engine/src/flutter/flow/layers/layer.h | 63 ++++++ .../src/flutter/flow/layers/opacity_layer.cc | 30 ++- .../src/flutter/flow/layers/opacity_layer.h | 13 ++ .../flow/layers/opacity_layer_unittests.cc | 12 +- .../src/flutter/flow/layers/picture_layer.cc | 19 +- .../flutter/flow/layers/shader_mask_layer.cc | 9 +- .../src/flutter/flow/layers/texture_layer.cc | 7 +- .../flutter/flow/layers/transform_layer.cc | 1 + engine/src/flutter/flow/raster_cache.cc | 13 +- engine/src/flutter/flow/raster_cache.h | 10 +- .../src/flutter/flow/testing/mock_texture.cc | 9 +- .../src/flutter/flow/testing/mock_texture.h | 4 +- .../flutter/shell/common/shell_unittests.cc | 3 +- .../android/android_external_texture_gl.cc | 5 +- .../android/android_external_texture_gl.h | 3 +- .../FlutterDarwinExternalTextureMetal.h | 11 +- .../FlutterDarwinExternalTextureMetal.mm | 13 +- .../darwin/ios/ios_external_texture_gl.h | 3 +- .../darwin/ios/ios_external_texture_gl.mm | 5 +- .../darwin/ios/ios_external_texture_metal.h | 3 +- .../darwin/ios/ios_external_texture_metal.mm | 14 +- .../embedder/embedder_external_texture_gl.cc | 7 +- .../embedder/embedder_external_texture_gl.h | 3 +- .../embedder_external_texture_metal.h | 3 +- .../embedder_external_texture_metal.mm | 7 +- .../platform/embedder/fixtures/main.dart | 2 + .../verifyb143464703_soft_noxform.png | Bin 6985 -> 6981 bytes 42 files changed, 854 insertions(+), 173 deletions(-) diff --git a/engine/src/flutter/common/graphics/texture.h b/engine/src/flutter/common/graphics/texture.h index 5ed8dafa668..57afa60a636 100644 --- a/engine/src/flutter/common/graphics/texture.h +++ b/engine/src/flutter/common/graphics/texture.h @@ -26,7 +26,8 @@ class Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) = 0; + const SkSamplingOptions& sampling, + const SkPaint* paint = nullptr) = 0; // Called from raster thread. virtual void OnGrContextCreated() = 0; diff --git a/engine/src/flutter/flow/display_list.cc b/engine/src/flutter/flow/display_list.cc index 4ed97a8766e..d9e22486373 100644 --- a/engine/src/flutter/flow/display_list.cc +++ b/engine/src/flutter/flow/display_list.cc @@ -978,8 +978,8 @@ static bool CompareOps(uint8_t* ptrA, return true; } -void DisplayList::RenderTo(SkCanvas* canvas) const { - DisplayListCanvasDispatcher dispatcher(canvas); +void DisplayList::RenderTo(SkCanvas* canvas, SkScalar opacity) const { + DisplayListCanvasDispatcher dispatcher(canvas, opacity); Dispatch(dispatcher); } @@ -995,19 +995,31 @@ bool DisplayList::Equals(const DisplayList& other) const { return CompareOps(ptr, ptr + byte_count_, o_ptr, o_ptr + other.byte_count_); } +DisplayList::DisplayList() + : byte_count_(0), + op_count_(0), + nested_byte_count_(0), + nested_op_count_(0), + unique_id_(0), + bounds_({0, 0, 0, 0}), + bounds_cull_({0, 0, 0, 0}), + can_apply_group_opacity_(true) {} + DisplayList::DisplayList(uint8_t* ptr, size_t byte_count, int op_count, size_t nested_byte_count, int nested_op_count, - const SkRect& cull_rect) + const SkRect& cull_rect, + bool can_apply_group_opacity) : storage_(ptr), byte_count_(byte_count), op_count_(op_count), nested_byte_count_(nested_byte_count), nested_op_count_(nested_op_count), bounds_({0, 0, -1, -1}), - bounds_cull_(cull_rect) { + bounds_cull_(cull_rect), + can_apply_group_opacity_(can_apply_group_opacity) { static std::atomic nextID{1}; do { unique_id_ = nextID.fetch_add(+1, std::memory_order_relaxed); @@ -1057,7 +1069,7 @@ void* DisplayListBuilder::Push(size_t pod, int op_inc, Args&&... args) { } sk_sp DisplayListBuilder::Build() { - while (save_level_ > 0) { + while (layer_stack_.size() > 1) { restore(); } size_t bytes = used_; @@ -1067,13 +1079,17 @@ sk_sp DisplayListBuilder::Build() { used_ = allocated_ = op_count_ = 0; nested_bytes_ = nested_op_count_ = 0; storage_.realloc(bytes); + bool compatible = layer_stack_.back().is_group_opacity_compatible(); return sk_sp(new DisplayList(storage_.release(), bytes, count, nested_bytes, nested_count, - cull_rect_)); + cull_rect_, compatible)); } DisplayListBuilder::DisplayListBuilder(const SkRect& cull_rect) - : cull_rect_(cull_rect) {} + : cull_rect_(cull_rect) { + layer_stack_.emplace_back(); + current_layer_ = &layer_stack_.back(); +} DisplayListBuilder::~DisplayListBuilder() { uint8_t* ptr = storage_.get(); @@ -1090,6 +1106,7 @@ void DisplayListBuilder::onSetDither(bool dither) { } void DisplayListBuilder::onSetInvertColors(bool invert) { Push(0, 0, current_invert_colors_ = invert); + UpdateCurrentOpacityCompatibility(); } void DisplayListBuilder::onSetStrokeCap(SkPaint::Cap cap) { Push(0, 0, current_stroke_cap_ = cap); @@ -1112,6 +1129,7 @@ void DisplayListBuilder::onSetColor(SkColor color) { void DisplayListBuilder::onSetBlendMode(SkBlendMode mode) { current_blender_ = nullptr; Push(0, 0, current_blend_mode_ = mode); + UpdateCurrentOpacityCompatibility(); } void DisplayListBuilder::onSetBlender(sk_sp blender) { // setBlender(nullptr) should be redirected to setBlendMode(SrcOver) @@ -1126,6 +1144,7 @@ void DisplayListBuilder::onSetBlender(sk_sp blender) { (current_blender_ = blender) // ? Push(0, 0, std::move(blender)) : Push(0, 0); + UpdateCurrentOpacityCompatibility(); } } void DisplayListBuilder::onSetShader(sk_sp shader) { @@ -1142,6 +1161,7 @@ void DisplayListBuilder::onSetColorFilter(sk_sp filter) { (current_color_filter_ = filter) // ? Push(0, 0, std::move(filter)) : Push(0, 0); + UpdateCurrentOpacityCompatibility(); } void DisplayListBuilder::onSetPathEffect(sk_sp effect) { (current_path_effect_ = effect) // @@ -1228,21 +1248,37 @@ void DisplayListBuilder::setAttributesFromPaint( } void DisplayListBuilder::save() { - save_level_++; Push(0, 1); + layer_stack_.emplace_back(); + current_layer_ = &layer_stack_.back(); } void DisplayListBuilder::restore() { - if (save_level_ > 0) { + if (layer_stack_.size() > 1) { + // Grab the current layer info before we push the restore + // on the stack. + LayerInfo layer_info = layer_stack_.back(); + layer_stack_.pop_back(); + current_layer_ = &layer_stack_.back(); Push(0, 1); - save_level_--; + if (!layer_info.has_layer) { + // For regular save() ops there was no protecting layer so we have to + // accumulate the values into the enclosing layer. + if (layer_info.cannot_inherit_opacity) { + current_layer_->mark_incompatible(); + } else if (layer_info.has_compatible_op) { + current_layer_->add_compatible_op(); + } + } } } void DisplayListBuilder::saveLayer(const SkRect* bounds, bool restore_with_paint) { - save_level_++; bounds // ? Push(0, 1, *bounds, restore_with_paint) : Push(0, 1, restore_with_paint); + CheckLayerOpacityCompatibility(restore_with_paint); + layer_stack_.emplace_back(true); + current_layer_ = &layer_stack_.back(); } void DisplayListBuilder::translate(SkScalar tx, SkScalar ty) { @@ -1356,21 +1392,27 @@ void DisplayListBuilder::clipPath(const SkPath& path, void DisplayListBuilder::drawPaint() { Push(0, 1); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawColor(SkColor color, SkBlendMode mode) { Push(0, 1, color, mode); + CheckLayerOpacityCompatibility(mode); } void DisplayListBuilder::drawLine(const SkPoint& p0, const SkPoint& p1) { Push(0, 1, p0, p1); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawRect(const SkRect& rect) { Push(0, 1, rect); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawOval(const SkRect& bounds) { Push(0, 1, bounds); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawCircle(const SkPoint& center, SkScalar radius) { Push(0, 1, center, radius); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawRRect(const SkRRect& rrect) { if (rrect.isRect()) { @@ -1379,14 +1421,17 @@ void DisplayListBuilder::drawRRect(const SkRRect& rrect) { drawOval(rrect.rect()); } else { Push(0, 1, rrect); + CheckLayerOpacityCompatibility(); } } void DisplayListBuilder::drawDRRect(const SkRRect& outer, const SkRRect& inner) { Push(0, 1, outer, inner); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawPath(const SkPath& path) { Push(0, 1, path); + CheckLayerOpacityHairlineCompatibility(); } void DisplayListBuilder::drawArc(const SkRect& bounds, @@ -1394,6 +1439,11 @@ void DisplayListBuilder::drawArc(const SkRect& bounds, SkScalar sweep, bool useCenter) { Push(0, 1, bounds, start, sweep, useCenter); + if (useCenter) { + CheckLayerOpacityHairlineCompatibility(); + } else { + CheckLayerOpacityCompatibility(); + } } void DisplayListBuilder::drawPoints(SkCanvas::PointMode mode, uint32_t count, @@ -1416,10 +1466,19 @@ void DisplayListBuilder::drawPoints(SkCanvas::PointMode mode, return; } CopyV(data_ptr, pts, count); + // drawPoints treats every point or line (or segment of a polygon) + // as a completely separate operation meaning we cannot ensure + // distribution of group opacity without analyzing the mode and the + // bounds of every sub-primitive. + // See: https://fiddle.skia.org/c/228459001d2de8db117ce25ef5cedb0c + UpdateLayerOpacityCompatibility(false); } void DisplayListBuilder::drawVertices(const sk_sp vertices, SkBlendMode mode) { Push(0, 1, std::move(vertices), mode); + // DrawVertices applies its colors to the paint so we have no way + // of controlling opacity using the current paint attributes. + UpdateLayerOpacityCompatibility(false); } void DisplayListBuilder::drawImage(const sk_sp image, @@ -1429,6 +1488,7 @@ void DisplayListBuilder::drawImage(const sk_sp image, render_with_attributes ? Push(0, 1, std::move(image), point, sampling) : Push(0, 1, std::move(image), point, sampling); + CheckLayerOpacityCompatibility(render_with_attributes); } void DisplayListBuilder::drawImageRect(const sk_sp image, const SkRect& src, @@ -1438,6 +1498,7 @@ void DisplayListBuilder::drawImageRect(const sk_sp image, SkCanvas::SrcRectConstraint constraint) { Push(0, 1, std::move(image), src, dst, sampling, render_with_attributes, constraint); + CheckLayerOpacityCompatibility(render_with_attributes); } void DisplayListBuilder::drawImageNine(const sk_sp image, const SkIRect& center, @@ -1448,6 +1509,7 @@ void DisplayListBuilder::drawImageNine(const sk_sp image, ? Push(0, 1, std::move(image), center, dst, filter) : Push(0, 1, std::move(image), center, dst, filter); + CheckLayerOpacityCompatibility(render_with_attributes); } void DisplayListBuilder::drawImageLattice(const sk_sp image, const SkCanvas::Lattice& lattice, @@ -1469,6 +1531,7 @@ void DisplayListBuilder::drawImageLattice(const sk_sp image, filter, render_with_attributes); CopyV(pod, lattice.fXDivs, xDivCount, lattice.fYDivs, yDivCount, lattice.fColors, cellCount, lattice.fRectTypes, cellCount); + CheckLayerOpacityCompatibility(render_with_attributes); } void DisplayListBuilder::drawAtlas(const sk_sp atlas, const SkRSXform xform[], @@ -1503,6 +1566,10 @@ void DisplayListBuilder::drawAtlas(const sk_sp atlas, } CopyV(data_ptr, xform, count, tex, count); } + // drawAtlas treats each image as a separate operation so we cannot rely + // on it to distribute the opacity without overlap without checking all + // of the transforms and texture rectangles. + UpdateLayerOpacityCompatibility(false); } void DisplayListBuilder::drawPicture(const sk_sp picture, @@ -1520,6 +1587,7 @@ void DisplayListBuilder::drawPicture(const sk_sp picture, // This behavior is identical to the way SkPicture computes nested op counts. nested_op_count_ += picture->approximateOpCount(true) - 1; nested_bytes_ += picture->approximateBytesUsed(); + CheckLayerOpacityCompatibility(render_with_attributes); } void DisplayListBuilder::drawDisplayList( const sk_sp display_list) { @@ -1532,11 +1600,13 @@ void DisplayListBuilder::drawDisplayList( // This behavior is identical to the way SkPicture computes nested op counts. nested_op_count_ += display_list->op_count(true) - 1; nested_bytes_ += display_list->bytes(true); + UpdateLayerOpacityCompatibility(display_list->can_apply_group_opacity()); } void DisplayListBuilder::drawTextBlob(const sk_sp blob, SkScalar x, SkScalar y) { Push(0, 1, std::move(blob), x, y); + CheckLayerOpacityCompatibility(); } void DisplayListBuilder::drawShadow(const SkPath& path, const SkColor color, @@ -1546,6 +1616,7 @@ void DisplayListBuilder::drawShadow(const SkPath& path, transparent_occluder // ? Push(0, 1, path, color, elevation, dpr) : Push(0, 1, path, color, elevation, dpr); + UpdateLayerOpacityCompatibility(false); } // clang-format off diff --git a/engine/src/flutter/flow/display_list.h b/engine/src/flutter/flow/display_list.h index d7b439face4..89c3478c232 100644 --- a/engine/src/flutter/flow/display_list.h +++ b/engine/src/flutter/flow/display_list.h @@ -171,15 +171,7 @@ class DisplayList : public SkRefCnt { static const SkSamplingOptions MipmapSampling; static const SkSamplingOptions CubicSampling; - DisplayList() - : byte_count_(0), - op_count_(0), - nested_byte_count_(0), - nested_op_count_(0), - unique_id_(0), - bounds_({0, 0, 0, 0}), - bounds_cull_({0, 0, 0, 0}) {} - + DisplayList(); ~DisplayList(); void Dispatch(Dispatcher& ctx) const { @@ -187,7 +179,7 @@ class DisplayList : public SkRefCnt { Dispatch(ctx, ptr, ptr + byte_count_); } - void RenderTo(SkCanvas* canvas) const; + void RenderTo(SkCanvas* canvas, SkScalar opacity = SK_Scalar1) const; // SkPicture always includes nested bytes, but nested ops are // only included if requested. The defaults used here for these @@ -212,13 +204,16 @@ class DisplayList : public SkRefCnt { bool Equals(const DisplayList& other) const; + bool can_apply_group_opacity() { return can_apply_group_opacity_; } + private: DisplayList(uint8_t* ptr, size_t byte_count, int op_count, size_t nested_byte_count, int nested_op_count, - const SkRect& cull_rect); + const SkRect& cull_rect, + bool can_apply_group_opacity); std::unique_ptr> storage_; size_t byte_count_; @@ -233,6 +228,8 @@ class DisplayList : public SkRefCnt { // Only used for drawPaint() and drawColor() SkRect bounds_cull_; + bool can_apply_group_opacity_; + void ComputeBounds(); void Dispatch(Dispatcher& ctx, uint8_t* ptr, uint8_t* end) const; @@ -548,6 +545,13 @@ class DisplayListFlags { static constexpr int kUsesMaskFilter_ = 1 << 18; static constexpr int kUsesImageFilter_ = 1 << 19; + // Some ops have an optional paint argument. If the version + // stored in the DisplayList ignores the paint, but there + // is an option to render the same op with a paint then + // both of the following flags are set to indicate that + // a default paint object can be constructed when rendering + // the op to carry information imposed from outside the + // DisplayList (for example, the opacity override). static constexpr int kIgnoresPaint_ = 1 << 30; // clang-format on @@ -746,7 +750,9 @@ class DisplayListOpFlags : DisplayListFlags { // If there is some code that already renders to an SkCanvas object, // those rendering commands can be captured into a DisplayList using // the DisplayListCanvasRecorder class. -class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { +class DisplayListBuilder final : public virtual Dispatcher, + public SkRefCnt, + DisplayListOpFlags { public: explicit DisplayListBuilder(const SkRect& cull_rect = kMaxCullRect_); ~DisplayListBuilder(); @@ -880,7 +886,7 @@ class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { void save() override; void saveLayer(const SkRect* bounds, bool restore_with_paint) override; void restore() override; - int getSaveCount() { return save_level_ + 1; } + int getSaveCount() { return layer_stack_.size(); } void translate(SkScalar tx, SkScalar ty) override; void scale(SkScalar sx, SkScalar sy) override; @@ -977,7 +983,6 @@ class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { size_t used_ = 0; size_t allocated_ = 0; int op_count_ = 0; - int save_level_ = 0; // bytes and ops from |drawPicture| and |drawDisplayList| size_t nested_bytes_ = 0; @@ -996,6 +1001,94 @@ class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { return SkScalarIsFinite(sigma) && sigma > 0.0; } + struct LayerInfo { + LayerInfo(bool has_layer = false) + : has_layer(has_layer), + cannot_inherit_opacity(false), + has_compatible_op(false) {} + + bool has_layer; + bool cannot_inherit_opacity; + bool has_compatible_op; + + bool is_group_opacity_compatible() const { return !cannot_inherit_opacity; } + + void mark_incompatible() { cannot_inherit_opacity = true; } + + // For now this only allows a single compatible op to mark the + // layer as being compatible with group opacity. If we start + // computing bounds of ops in the Builder methods then we + // can upgrade this to checking for overlapping ops. + // See https://github.com/flutter/flutter/issues/93899 + void add_compatible_op() { + if (!cannot_inherit_opacity) { + if (has_compatible_op) { + cannot_inherit_opacity = true; + } else { + has_compatible_op = true; + } + } + } + }; + + std::vector layer_stack_; + LayerInfo* current_layer_; + + // This flag indicates whether or not the current rendering attributes + // are compatible with rendering ops applying an inherited opacity. + bool current_opacity_compatibility_ = true; + + // Returns the compatibility of a given blend mode for applying an + // inherited opacity value to modulate the visibility of the op. + // For now we only accept SrcOver blend modes but this could be expanded + // in the future to include other (rarely used) modes that also modulate + // the opacity of a rendering operation at the cost of a switch statement + // or lookup table. + static bool IsOpacityCompatible(SkBlendMode mode) { + return (mode == SkBlendMode::kSrcOver); + } + + void UpdateCurrentOpacityCompatibility() { + current_opacity_compatibility_ = // + current_color_filter_ == nullptr && // + !current_invert_colors_ && // + current_blender_ == nullptr && // + IsOpacityCompatible(current_blend_mode_); + } + + // Update the opacity compatibility flags of the current layer for an op + // that has determined its compatibility as indicated by |compatible|. + void UpdateLayerOpacityCompatibility(bool compatible) { + if (compatible) { + current_layer_->add_compatible_op(); + } else { + current_layer_->mark_incompatible(); + } + } + + // Check for opacity compatibility for an op that may or may not use the + // current rendering attributes as indicated by |uses_blend_attribute|. + // If the flag is false then the rendering op will be able to substitute + // a default Paint object with the opacity applied using the default SrcOver + // blend mode which is always compatible with applying an inherited opacity. + void CheckLayerOpacityCompatibility(bool uses_blend_attribute = true) { + UpdateLayerOpacityCompatibility(!uses_blend_attribute || + current_opacity_compatibility_); + } + + void CheckLayerOpacityHairlineCompatibility() { + UpdateLayerOpacityCompatibility( + current_opacity_compatibility_ && + (current_style_ == SkPaint::kFill_Style || current_stroke_width_ > 0)); + } + + // Check for opacity compatibility for an op that ignores the current + // attributes and uses the indicated blend |mode| to render to the layer. + // This is only used by |drawColor| currently. + void CheckLayerOpacityCompatibility(SkBlendMode mode) { + UpdateLayerOpacityCompatibility(IsOpacityCompatible(mode)); + } + void onSetAntiAlias(bool aa); void onSetDither(bool dither); void onSetInvertColors(bool invert); diff --git a/engine/src/flutter/flow/display_list_canvas.cc b/engine/src/flutter/flow/display_list_canvas.cc index 7f66a37515e..eeda70d5727 100644 --- a/engine/src/flutter/flow/display_list_canvas.cc +++ b/engine/src/flutter/flow/display_list_canvas.cc @@ -11,16 +11,32 @@ namespace flutter { +const SkPaint* DisplayListCanvasDispatcher::safe_paint(bool use_attributes) { + if (use_attributes) { + // The accumulated SkPaint object will already have incorporated + // any attribute overrides. + return &paint(); + } else if (has_opacity()) { + temp_paint_.setAlphaf(opacity()); + return &temp_paint_; + } else { + return nullptr; + } +} + void DisplayListCanvasDispatcher::save() { canvas_->save(); + save_opacity(false); } void DisplayListCanvasDispatcher::restore() { canvas_->restore(); + restore_opacity(); } void DisplayListCanvasDispatcher::saveLayer(const SkRect* bounds, bool restore_with_paint) { TRACE_EVENT0("flutter", "Canvas::saveLayer"); - canvas_->saveLayer(bounds, restore_with_paint ? &paint() : nullptr); + canvas_->saveLayer(bounds, safe_paint(restore_with_paint)); + save_opacity(true); } void DisplayListCanvasDispatcher::translate(SkScalar tx, SkScalar ty) { @@ -87,7 +103,11 @@ void DisplayListCanvasDispatcher::drawPaint() { canvas_->drawPaint(sk_paint); } void DisplayListCanvasDispatcher::drawColor(SkColor color, SkBlendMode mode) { - canvas_->drawColor(color, mode); + // SkCanvas::drawColor(SkColor) does the following conversion anyway + // We do it here manually to increase precision on applying opacity + SkColor4f color4f = SkColor4f::FromColor(color); + color4f.fA *= opacity(); + canvas_->drawColor(color4f, mode); } void DisplayListCanvasDispatcher::drawLine(const SkPoint& p0, const SkPoint& p1) { @@ -133,7 +153,7 @@ void DisplayListCanvasDispatcher::drawImage(const sk_sp image, const SkSamplingOptions& sampling, bool render_with_attributes) { canvas_->drawImage(image, point.fX, point.fY, sampling, - render_with_attributes ? &paint() : nullptr); + safe_paint(render_with_attributes)); } void DisplayListCanvasDispatcher::drawImageRect( const sk_sp image, @@ -143,8 +163,7 @@ void DisplayListCanvasDispatcher::drawImageRect( bool render_with_attributes, SkCanvas::SrcRectConstraint constraint) { canvas_->drawImageRect(image, src, dst, sampling, - render_with_attributes ? &paint() : nullptr, - constraint); + safe_paint(render_with_attributes), constraint); } void DisplayListCanvasDispatcher::drawImageNine(const sk_sp image, const SkIRect& center, @@ -152,7 +171,7 @@ void DisplayListCanvasDispatcher::drawImageNine(const sk_sp image, SkFilterMode filter, bool render_with_attributes) { canvas_->drawImageNine(image.get(), center, dst, filter, - render_with_attributes ? &paint() : nullptr); + safe_paint(render_with_attributes)); } void DisplayListCanvasDispatcher::drawImageLattice( const sk_sp image, @@ -161,7 +180,7 @@ void DisplayListCanvasDispatcher::drawImageLattice( SkFilterMode filter, bool render_with_attributes) { canvas_->drawImageLattice(image.get(), lattice, dst, filter, - render_with_attributes ? &paint() : nullptr); + safe_paint(render_with_attributes)); } void DisplayListCanvasDispatcher::drawAtlas(const sk_sp atlas, const SkRSXform xform[], @@ -173,15 +192,16 @@ void DisplayListCanvasDispatcher::drawAtlas(const sk_sp atlas, const SkRect* cullRect, bool render_with_attributes) { canvas_->drawAtlas(atlas.get(), xform, tex, colors, count, mode, sampling, - cullRect, render_with_attributes ? &paint() : nullptr); + cullRect, safe_paint(render_with_attributes)); } void DisplayListCanvasDispatcher::drawPicture(const sk_sp picture, const SkMatrix* matrix, bool render_with_attributes) { - if (render_with_attributes) { + const SkPaint* paint = safe_paint(render_with_attributes); + if (paint) { // drawPicture does an implicit saveLayer if an SkPaint is supplied. TRACE_EVENT0("flutter", "Canvas::saveLayer"); - canvas_->drawPicture(picture, matrix, &paint()); + canvas_->drawPicture(picture, matrix, paint); } else { canvas_->drawPicture(picture, matrix, nullptr); } @@ -189,10 +209,7 @@ void DisplayListCanvasDispatcher::drawPicture(const sk_sp picture, void DisplayListCanvasDispatcher::drawDisplayList( const sk_sp display_list) { int save_count = canvas_->save(); - { - DisplayListCanvasDispatcher dispatcher(canvas_); - display_list->Dispatch(dispatcher); - } + display_list->RenderTo(canvas_, opacity()); canvas_->restoreToCount(save_count); } void DisplayListCanvasDispatcher::drawTextBlob(const sk_sp blob, diff --git a/engine/src/flutter/flow/display_list_canvas.h b/engine/src/flutter/flow/display_list_canvas.h index 6c4d0f77275..fd813e777a8 100644 --- a/engine/src/flutter/flow/display_list_canvas.h +++ b/engine/src/flutter/flow/display_list_canvas.h @@ -27,7 +27,11 @@ namespace flutter { class DisplayListCanvasDispatcher : public virtual Dispatcher, public SkPaintDispatchHelper { public: - explicit DisplayListCanvasDispatcher(SkCanvas* canvas) : canvas_(canvas) {} + explicit DisplayListCanvasDispatcher(SkCanvas* canvas, + SkScalar opacity = SK_Scalar1) + : SkPaintDispatchHelper(opacity), canvas_(canvas) {} + + const SkPaint* safe_paint(bool use_attributes); void save() override; void restore() override; @@ -115,6 +119,7 @@ class DisplayListCanvasDispatcher : public virtual Dispatcher, private: SkCanvas* canvas_; + SkPaint temp_paint_; }; // Receives all methods on SkCanvas and sends them to a DisplayListBuilder diff --git a/engine/src/flutter/flow/display_list_canvas_unittests.cc b/engine/src/flutter/flow/display_list_canvas_unittests.cc index 4352c01008a..088bd34e98f 100644 --- a/engine/src/flutter/flow/display_list_canvas_unittests.cc +++ b/engine/src/flutter/flow/display_list_canvas_unittests.cc @@ -244,13 +244,16 @@ static void EmptyDlRenderer(DisplayListBuilder&) {} class RenderSurface { public: - explicit RenderSurface(sk_sp surface) : surface_(surface) {} + explicit RenderSurface(sk_sp surface) : surface_(surface) { + EXPECT_EQ(canvas()->save(), 1); + } ~RenderSurface() { sk_free(addr_); } SkCanvas* canvas() { return surface_->getCanvas(); } const SkPixmap* pixmap() { if (!pixmap_.addr()) { + canvas()->restoreToCount(1); SkImageInfo info = surface_->imageInfo(); if (info.colorType() != kN32_SkColorType || !surface_->peekPixels(&pixmap_)) { @@ -280,13 +283,14 @@ class RenderEnvironment { return RenderEnvironment(SkImageInfo::MakeN32Premul(1, 1)); } - RenderSurface MakeSurface(const SkColor bg = SK_ColorTRANSPARENT, - int width = TestWidth, - int height = TestHeight) const { + std::unique_ptr MakeSurface( + const SkColor bg = SK_ColorTRANSPARENT, + int width = TestWidth, + int height = TestHeight) const { sk_sp surface = SkSurface::MakeRaster(info_.makeWH(width, height)); surface->getCanvas()->clear(bg); - return RenderSurface(surface); + return std::make_unique(surface); } void init_ref(CvRenderer& cv_renderer, SkColor bg = SK_ColorTRANSPARENT) { @@ -301,25 +305,27 @@ class RenderEnvironment { ref_matrix_ = ref_canvas()->getTotalMatrix(); ref_clip_ = ref_canvas()->getDeviceClipBounds(); cv_renderer(ref_canvas(), ref_paint_); - ref_pixmap_ = ref_surface_.pixmap(); + ref_pixmap_ = ref_surface_->pixmap(); } - SkCanvas* ref_canvas() { return ref_surface_.canvas(); } + const SkImageInfo& info() const { return info_; } + SkCanvas* ref_canvas() { return ref_surface_->canvas(); } const SkPaint& ref_paint() const { return ref_paint_; } const SkMatrix& ref_matrix() const { return ref_matrix_; } const SkIRect& ref_clip_bounds() const { return ref_clip_; } const SkPixmap* ref_pixmap() const { return ref_pixmap_; } private: - explicit RenderEnvironment(const SkImageInfo& info) - : info_(info), ref_surface_(MakeSurface()) {} + explicit RenderEnvironment(const SkImageInfo& info) : info_(info) { + ref_surface_ = MakeSurface(); + } const SkImageInfo info_; SkPaint ref_paint_; SkMatrix ref_matrix_; SkIRect ref_clip_; - RenderSurface ref_surface_; + std::unique_ptr ref_surface_; const SkPixmap* ref_pixmap_ = nullptr; }; @@ -1280,7 +1286,7 @@ class CanvasCompareTester { } static void RenderWithStrokes(const TestParameters& testP, - const RenderEnvironment env, + 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 @@ -1512,8 +1518,16 @@ class CanvasCompareTester { [=](SkCanvas* c, SkPaint&) { c->skew(0.05, 0.05); }, [=](DisplayListBuilder& b) { b.skew(0.05, 0.05); })); { - SkMatrix tx = SkMatrix::MakeAll(1.10, 0.10, 5, // - 0.05, 1.05, 10, // + // 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( @@ -1660,8 +1674,8 @@ class CanvasCompareTester { // DisplayList mechanisms are not involved in this operation const std::string info = caseP.info(); const SkColor bg = caseP.bg(); - RenderSurface sk_surface = env.MakeSurface(bg); - SkCanvas* sk_canvas = sk_surface.canvas(); + std::unique_ptr sk_surface = env.MakeSurface(bg); + SkCanvas* sk_canvas = sk_surface->canvas(); SkPaint sk_paint; caseP.cv_setup()(sk_canvas, sk_paint); SkMatrix sk_matrix = sk_canvas->getTotalMatrix(); @@ -1672,7 +1686,7 @@ class CanvasCompareTester { caseP.cv_restore()(sk_canvas, sk_paint); const sk_sp sk_picture = getSkPicture(testP, caseP); SkRect sk_bounds = sk_picture->cullRect(); - const SkPixmap* sk_pixels = sk_surface.pixmap(); + const SkPixmap* sk_pixels = sk_surface->pixmap(); ASSERT_EQ(sk_pixels->width(), TestWidth) << info; ASSERT_EQ(sk_pixels->height(), TestHeight) << info; ASSERT_EQ(sk_pixels->info().bytesPerPixel(), 4) << info; @@ -1692,7 +1706,6 @@ class CanvasCompareTester { // This sequence plays the provided equivalently constructed // DisplayList onto the SkCanvas of the surface // DisplayList => direct rendering - RenderSurface dl_surface = env.MakeSurface(bg); DisplayListBuilder builder(TestBounds); caseP.render_to(builder, testP); sk_sp display_list = builder.Build(); @@ -1729,10 +1742,16 @@ class CanvasCompareTester { << info; } - display_list->RenderTo(dl_surface.canvas()); - compareToReference(dl_surface.pixmap(), sk_pixels, + std::unique_ptr dl_surface = env.MakeSurface(bg); + display_list->RenderTo(dl_surface->canvas()); + compareToReference(dl_surface->pixmap(), sk_pixels, info + " (DisplayList built directly -> surface)", &dl_bounds, &tolerance, bg); + + if (display_list->can_apply_group_opacity()) { + checkGroupOpacity(env, display_list, dl_surface->pixmap(), + info + " with Group Opacity", bg); + } } // This test cannot work if the rendering is using shadows until @@ -1741,11 +1760,11 @@ class CanvasCompareTester { // This sequence renders SkCanvas calls to a DisplayList and then // plays them back on SkCanvas to SkSurface // SkCanvas calls => DisplayList => rendering - RenderSurface cv_dl_surface = env.MakeSurface(bg); + std::unique_ptr cv_dl_surface = env.MakeSurface(bg); DisplayListCanvasRecorder dl_recorder(TestBounds); caseP.render_to(&dl_recorder, testP); - dl_recorder.builder()->Build()->RenderTo(cv_dl_surface.canvas()); - compareToReference(cv_dl_surface.pixmap(), sk_pixels, + dl_recorder.builder()->Build()->RenderTo(cv_dl_surface->canvas()); + compareToReference(cv_dl_surface->pixmap(), sk_pixels, info + " (Skia calls -> DisplayList -> surface)", nullptr, nullptr, bg); } @@ -1766,12 +1785,12 @@ class CanvasCompareTester { caseP.render_to(ref_canvas, testP); sk_sp ref_x2_picture = sk_x2_recorder.finishRecordingAsPicture(); - RenderSurface ref_x2_surface = + std::unique_ptr ref_x2_surface = env.MakeSurface(bg, TestWidth2, TestHeight2); - SkCanvas* ref_x2_canvas = ref_x2_surface.canvas(); + SkCanvas* ref_x2_canvas = ref_x2_surface->canvas(); ref_x2_canvas->scale(TestScale, TestScale); ref_x2_picture->playback(ref_x2_canvas); - const SkPixmap* ref_x2_pixels = ref_x2_surface.pixmap(); + const SkPixmap* ref_x2_pixels = ref_x2_surface->pixmap(); ASSERT_EQ(ref_x2_pixels->width(), TestWidth2) << info; ASSERT_EQ(ref_x2_pixels->height(), TestHeight2) << info; ASSERT_EQ(ref_x2_pixels->info().bytesPerPixel(), 4) << info; @@ -1779,19 +1798,75 @@ class CanvasCompareTester { DisplayListBuilder builder_x2(TestBounds); caseP.render_to(builder_x2, testP); sk_sp display_list_x2 = builder_x2.Build(); - RenderSurface test_x2_surface = + std::unique_ptr test_x2_surface = env.MakeSurface(bg, TestWidth2, TestHeight2); - SkCanvas* test_x2_canvas = test_x2_surface.canvas(); + SkCanvas* test_x2_canvas = test_x2_surface->canvas(); test_x2_canvas->scale(TestScale, TestScale); display_list_x2->RenderTo(test_x2_canvas); - compareToReference(test_x2_surface.pixmap(), ref_x2_pixels, + compareToReference(test_x2_surface->pixmap(), ref_x2_pixels, info + " (Both rendered scaled 2x)", nullptr, nullptr, bg, TestWidth2, TestHeight2, false); } } + static void checkGroupOpacity(const RenderEnvironment& env, + sk_sp display_list, + const SkPixmap* ref_pixmap, + const std::string info, + SkColor bg) { + SkScalar opacity = 128.0 / 255.0; + + std::unique_ptr group_opacity_surface = env.MakeSurface(bg); + SkCanvas* group_opacity_canvas = group_opacity_surface->canvas(); + display_list->RenderTo(group_opacity_canvas, opacity); + const SkPixmap* group_opacity_pixmap = group_opacity_surface->pixmap(); + + ASSERT_EQ(group_opacity_pixmap->width(), TestWidth) << info; + ASSERT_EQ(group_opacity_pixmap->height(), TestHeight) << info; + ASSERT_EQ(group_opacity_pixmap->info().bytesPerPixel(), 4) << info; + + ASSERT_EQ(ref_pixmap->width(), TestWidth) << info; + ASSERT_EQ(ref_pixmap->height(), TestHeight) << info; + ASSERT_EQ(ref_pixmap->info().bytesPerPixel(), 4) << info; + + int pixels_touched = 0; + int pixels_different = 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 = env.info().bytesPerPixel() < 4 ? 9 : 2; + for (int y = 0; y < TestHeight; y++) { + const uint32_t* ref_row = ref_pixmap->addr32(0, y); + const uint32_t* test_row = group_opacity_pixmap->addr32(0, y); + for (int x = 0; x < TestWidth; x++) { + uint32_t ref_pixel = ref_row[x]; + uint32_t test_pixel = test_row[x]; + if (ref_pixel != bg || test_pixel != bg) { + pixels_touched++; + for (int i = 0; i < 32; i += 8) { + int ref_comp = (ref_pixel >> i) & 0xff; + int bg_comp = (bg >> 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) { + pixels_different++; + break; + } + } + } + } + } + ASSERT_GT(pixels_touched, 20) << info; + ASSERT_LE(pixels_different, 1) << info; + } + static void checkPixels(const SkPixmap* ref_pixels, - SkRect ref_bounds, + const SkRect ref_bounds, const std::string info, const SkColor bg) { SkPMColor untouched = SkPreMultiplyColor(bg); @@ -2844,7 +2919,7 @@ TEST_F(DisplayListCanvas, DrawPicture) { TEST_F(DisplayListCanvas, DrawPictureWithMatrix) { sk_sp picture = makeTestPicture(); - SkMatrix matrix = SkMatrix::Scale(0.95, 0.95); + SkMatrix matrix = SkMatrix::Scale(0.9, 0.9); CanvasCompareTester::RenderAll( // TestParameters( [=](SkCanvas* canvas, const SkPaint& paint) { // diff --git a/engine/src/flutter/flow/display_list_unittests.cc b/engine/src/flutter/flow/display_list_unittests.cc index 6228440a0f2..cb8b7c21c41 100644 --- a/engine/src/flutter/flow/display_list_unittests.cc +++ b/engine/src/flutter/flow/display_list_unittests.cc @@ -232,6 +232,7 @@ struct DisplayListInvocation { size_t sk_byte_count_; DlInvoker invoker; + bool supports_group_opacity_ = false; bool sk_version_matches() { return (op_count_ == sk_op_count_ && byte_count_ == sk_byte_count_); @@ -246,6 +247,8 @@ struct DisplayListInvocation { bool is_empty() { return byte_count_ == 0; } + bool supports_group_opacity() { return supports_group_opacity_; } + int op_count() { return op_count_; } // byte count for the individual ops, no DisplayList overhead size_t raw_byte_count() { return byte_count_; } @@ -1397,5 +1400,183 @@ TEST(DisplayList, SetMaskFilterNullResetsMaskFilter) { ASSERT_EQ(display_list->bytes(), sizeof(DisplayList) + 8u + 24u + 8u + 24u); } +TEST(DisplayList, SingleOpsMightSupportGroupOpacityWithOrWithoutBlendMode) { + auto run_tests = [](std::string name, + void build(DisplayListBuilder & builder), + bool expect_for_op, bool expect_with_kSrc) { + { + // First test is the draw op, by itself + // (usually supports group opacity) + DisplayListBuilder builder; + build(builder); + auto display_list = builder.Build(); + EXPECT_EQ(display_list->can_apply_group_opacity(), expect_for_op) + << "{" << std::endl + << " " << name << std::endl + << "}"; + } + { + // Second test i the draw op with kSrc, + // (usually fails group opacity) + DisplayListBuilder builder; + builder.setBlendMode(SkBlendMode::kSrc); + build(builder); + auto display_list = builder.Build(); + EXPECT_EQ(display_list->can_apply_group_opacity(), expect_with_kSrc) + << "{" << std::endl + << " builder.setBlendMode(kSrc);" << std::endl + << " " << name << std::endl + << "}"; + } + }; + +#define RUN_TESTS(body) \ + run_tests( \ + #body, [](DisplayListBuilder& builder) { body }, true, false) +#define RUN_TESTS2(body, expect) \ + run_tests( \ + #body, [](DisplayListBuilder& builder) { body }, expect, expect) + + RUN_TESTS(builder.drawPaint();); + RUN_TESTS2(builder.drawColor(SK_ColorRED, SkBlendMode::kSrcOver);, true); + RUN_TESTS2(builder.drawColor(SK_ColorRED, SkBlendMode::kSrc);, false); + RUN_TESTS(builder.drawLine({0, 0}, {10, 10});); + RUN_TESTS(builder.drawRect({0, 0, 10, 10});); + RUN_TESTS(builder.drawOval({0, 0, 10, 10});); + RUN_TESTS(builder.drawCircle({10, 10}, 5);); + RUN_TESTS(builder.drawRRect(SkRRect::MakeRectXY({0, 0, 10, 10}, 2, 2));); + RUN_TESTS(builder.drawDRRect(SkRRect::MakeRectXY({0, 0, 10, 10}, 2, 2), + SkRRect::MakeRectXY({2, 2, 8, 8}, 2, 2));); + RUN_TESTS(builder.drawPath( + SkPath().addOval({0, 0, 10, 10}).addOval({5, 5, 15, 15}));); + RUN_TESTS(builder.drawArc({0, 0, 10, 10}, 0, M_PI, true);); + RUN_TESTS2(builder.drawPoints(SkCanvas::kPoints_PointMode, TestPointCount, + TestPoints); + , false); + RUN_TESTS2(builder.drawVertices(TestVertices1, SkBlendMode::kSrc);, false); + RUN_TESTS(builder.drawImage(TestImage1, {0, 0}, DisplayList::LinearSampling, + true);); + RUN_TESTS2( + builder.drawImage(TestImage1, {0, 0}, DisplayList::LinearSampling, false); + , true); + RUN_TESTS(builder.drawImageRect(TestImage1, {10, 10, 20, 20}, {0, 0, 10, 10}, + DisplayList::NearestSampling, true);); + RUN_TESTS2(builder.drawImageRect(TestImage1, {10, 10, 20, 20}, {0, 0, 10, 10}, + DisplayList::NearestSampling, false); + , true); + RUN_TESTS(builder.drawImageNine(TestImage2, {20, 20, 30, 30}, {0, 0, 20, 20}, + SkFilterMode::kLinear, true);); + RUN_TESTS2(builder.drawImageNine(TestImage2, {20, 20, 30, 30}, {0, 0, 20, 20}, + SkFilterMode::kLinear, false); + , true); + RUN_TESTS(builder.drawImageLattice( + TestImage1, + {TestDivs1, TestDivs1, nullptr, 3, 3, &TestLatticeSrcRect, nullptr}, + {10, 10, 40, 40}, SkFilterMode::kNearest, true);); + RUN_TESTS2(builder.drawImageLattice( + TestImage1, + {TestDivs1, TestDivs1, nullptr, 3, 3, &TestLatticeSrcRect, nullptr}, + {10, 10, 40, 40}, SkFilterMode::kNearest, false); + , true); + static SkRSXform xforms[] = {{1, 0, 0, 0}, {0, 1, 0, 0}}; + static SkRect texs[] = {{10, 10, 20, 20}, {20, 20, 30, 30}}; + RUN_TESTS2(builder.drawAtlas(TestImage1, xforms, texs, nullptr, 2, + SkBlendMode::kSrcIn, + DisplayList::NearestSampling, nullptr, true); + , false); + RUN_TESTS2(builder.drawAtlas(TestImage1, xforms, texs, nullptr, 2, + SkBlendMode::kSrcIn, + DisplayList::NearestSampling, nullptr, false); + , false); + RUN_TESTS(builder.drawPicture(TestPicture1, nullptr, true);); + RUN_TESTS2(builder.drawPicture(TestPicture1, nullptr, false);, true); + EXPECT_TRUE(TestDisplayList1->can_apply_group_opacity()); + RUN_TESTS2(builder.drawDisplayList(TestDisplayList1);, true); + { + static DisplayListBuilder builder; + builder.drawRect({0, 0, 10, 10}); + builder.drawRect({5, 5, 15, 15}); + static auto display_list = builder.Build(); + RUN_TESTS2(builder.drawDisplayList(display_list);, false); + } + RUN_TESTS(builder.drawTextBlob(TestBlob1, 0, 0);); + RUN_TESTS2(builder.drawShadow(TestPath1, SK_ColorBLACK, 1.0, false, 1.0); + , false); + +#undef RUN_TESTS2 +#undef RUN_TESTS +} + +TEST(DisplayList, OverlappingOpsDoNotSupportGroupOpacity) { + DisplayListBuilder builder; + for (int i = 0; i < 10; i++) { + builder.drawRect(SkRect::MakeXYWH(i * 10, 0, 30, 30)); + } + auto display_list = builder.Build(); + EXPECT_FALSE(display_list->can_apply_group_opacity()); +} + +TEST(DisplayList, SaveLayerFalseSupportsGroupOpacityWithOverlappingChidren) { + DisplayListBuilder builder; + builder.saveLayer(nullptr, false); + for (int i = 0; i < 10; i++) { + builder.drawRect(SkRect::MakeXYWH(i * 10, 0, 30, 30)); + } + builder.restore(); + auto display_list = builder.Build(); + EXPECT_TRUE(display_list->can_apply_group_opacity()); +} + +TEST(DisplayList, SaveLayerTrueSupportsGroupOpacityWithOverlappingChidren) { + DisplayListBuilder builder; + builder.saveLayer(nullptr, true); + for (int i = 0; i < 10; i++) { + builder.drawRect(SkRect::MakeXYWH(i * 10, 0, 30, 30)); + } + builder.restore(); + auto display_list = builder.Build(); + EXPECT_TRUE(display_list->can_apply_group_opacity()); +} + +TEST(DisplayList, SaveLayerFalseWithSrcBlendSupportsGroupOpacity) { + DisplayListBuilder builder; + builder.setBlendMode(SkBlendMode::kSrc); + builder.saveLayer(nullptr, false); + builder.drawRect({0, 0, 10, 10}); + builder.restore(); + auto display_list = builder.Build(); + EXPECT_TRUE(display_list->can_apply_group_opacity()); +} + +TEST(DisplayList, SaveLayerTrueWithSrcBlendDoesNotSupportGroupOpacity) { + DisplayListBuilder builder; + builder.setBlendMode(SkBlendMode::kSrc); + builder.saveLayer(nullptr, true); + builder.drawRect({0, 0, 10, 10}); + builder.restore(); + auto display_list = builder.Build(); + EXPECT_FALSE(display_list->can_apply_group_opacity()); +} + +TEST(DisplayList, SaveLayerFalseSupportsGroupOpacityWithChildSrcBlend) { + DisplayListBuilder builder; + builder.saveLayer(nullptr, false); + builder.setBlendMode(SkBlendMode::kSrc); + builder.drawRect({0, 0, 10, 10}); + builder.restore(); + auto display_list = builder.Build(); + EXPECT_TRUE(display_list->can_apply_group_opacity()); +} + +TEST(DisplayList, SaveLayerTrueSupportsGroupOpacityWithChildSrcBlend) { + DisplayListBuilder builder; + builder.saveLayer(nullptr, true); + builder.setBlendMode(SkBlendMode::kSrc); + builder.drawRect({0, 0, 10, 10}); + builder.restore(); + auto display_list = builder.Build(); + EXPECT_TRUE(display_list->can_apply_group_opacity()); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/flow/display_list_utils.cc b/engine/src/flutter/flow/display_list_utils.cc index 5532390cfb5..6fcf72cd278 100644 --- a/engine/src/flutter/flow/display_list_utils.cc +++ b/engine/src/flutter/flow/display_list_utils.cc @@ -26,6 +26,25 @@ constexpr float invert_color_matrix[20] = { }; // clang-format on +void SkPaintDispatchHelper::save_opacity(bool reset_and_restore) { + if (opacity_ >= SK_Scalar1) { + reset_and_restore = false; + } + save_stack_.emplace_back(opacity_, reset_and_restore); + if (reset_and_restore) { + opacity_ = SK_Scalar1; + setColor(current_color_); + } +} +void SkPaintDispatchHelper::restore_opacity() { + SaveInfo& info = save_stack_.back(); + if (info.restore_opacity) { + opacity_ = info.opacity; + setColor(current_color_); + } + save_stack_.pop_back(); +} + void SkPaintDispatchHelper::setAntiAlias(bool aa) { paint_.setAntiAlias(aa); } @@ -52,7 +71,11 @@ void SkPaintDispatchHelper::setStrokeMiter(SkScalar limit) { paint_.setStrokeMiter(limit); } void SkPaintDispatchHelper::setColor(SkColor color) { + current_color_ = color; paint_.setColor(color); + if (has_opacity()) { + paint_.setAlphaf(paint_.getAlphaf() * opacity()); + } } void SkPaintDispatchHelper::setBlendMode(SkBlendMode mode) { paint_.setBlendMode(mode); diff --git a/engine/src/flutter/flow/display_list_utils.h b/engine/src/flutter/flow/display_list_utils.h index db03a56511b..434c82dab0b 100644 --- a/engine/src/flutter/flow/display_list_utils.h +++ b/engine/src/flutter/flow/display_list_utils.h @@ -96,6 +96,13 @@ class IgnoreTransformDispatchHelper : public virtual Dispatcher { // which can be accessed at any time via paint(). class SkPaintDispatchHelper : public virtual Dispatcher { public: + SkPaintDispatchHelper(SkScalar opacity = SK_Scalar1) + : current_color_(SK_ColorBLACK), opacity_(opacity) { + if (opacity < SK_Scalar1) { + paint_.setAlphaf(opacity); + } + } + void setAntiAlias(bool aa) override; void setDither(bool dither) override; void setStyle(SkPaint::Style style) override; @@ -115,6 +122,12 @@ class SkPaintDispatchHelper : public virtual Dispatcher { void setImageFilter(sk_sp filter) override; const SkPaint& paint() { return paint_; } + SkScalar opacity() { return opacity_; } + bool has_opacity() { return opacity_ < SK_Scalar1; } + + protected: + void save_opacity(bool reset_and_restore); + void restore_opacity(); private: SkPaint paint_; @@ -122,6 +135,18 @@ class SkPaintDispatchHelper : public virtual Dispatcher { sk_sp color_filter_; sk_sp makeColorFilter(); + + struct SaveInfo { + SaveInfo(SkScalar opacity, bool restore_opacity) + : opacity(opacity), restore_opacity(restore_opacity) {} + + SkScalar opacity; + bool restore_opacity; + }; + std::vector save_stack_; + + SkColor current_color_; + SkScalar opacity_; }; class SkMatrixSource { diff --git a/engine/src/flutter/flow/layers/clip_path_layer.cc b/engine/src/flutter/flow/layers/clip_path_layer.cc index ca30baf2ac8..5a4ad304414 100644 --- a/engine/src/flutter/flow/layers/clip_path_layer.cc +++ b/engine/src/flutter/flow/layers/clip_path_layer.cc @@ -10,6 +10,7 @@ namespace flutter { ClipPathLayer::ClipPathLayer(const SkPath& clip_path, Clip clip_behavior) : clip_path_(clip_path), clip_behavior_(clip_behavior) { FML_DCHECK(clip_behavior != Clip::none); + set_layer_can_inherit_opacity(true); } void ClipPathLayer::Diff(DiffContext* context, const Layer* old_layer) { @@ -58,16 +59,20 @@ void ClipPathLayer::Paint(PaintContext& context) const { context.internal_nodes_canvas->clipPath(clip_path_, clip_behavior_ != Clip::hardEdge); - if (UsesSaveLayer()) { - TRACE_EVENT0("flutter", "Canvas::saveLayer"); - context.internal_nodes_canvas->saveLayer(paint_bounds(), nullptr); + if (!UsesSaveLayer()) { + PaintChildren(context); + return; } + + AutoCachePaint cache_paint(context); + TRACE_EVENT0("flutter", "Canvas::saveLayer"); + context.internal_nodes_canvas->saveLayer(paint_bounds(), cache_paint.paint()); + PaintChildren(context); - if (UsesSaveLayer()) { - context.internal_nodes_canvas->restore(); - if (context.checkerboard_offscreen_layers) { - DrawCheckerboard(context.internal_nodes_canvas, paint_bounds()); - } + + context.internal_nodes_canvas->restore(); + if (context.checkerboard_offscreen_layers) { + DrawCheckerboard(context.internal_nodes_canvas, paint_bounds()); } } diff --git a/engine/src/flutter/flow/layers/clip_rect_layer.cc b/engine/src/flutter/flow/layers/clip_rect_layer.cc index 8504d41ed15..cdb5531283b 100644 --- a/engine/src/flutter/flow/layers/clip_rect_layer.cc +++ b/engine/src/flutter/flow/layers/clip_rect_layer.cc @@ -10,6 +10,7 @@ namespace flutter { ClipRectLayer::ClipRectLayer(const SkRect& clip_rect, Clip clip_behavior) : clip_rect_(clip_rect), clip_behavior_(clip_behavior) { FML_DCHECK(clip_behavior != Clip::none); + set_layer_can_inherit_opacity(true); } void ClipRectLayer::Diff(DiffContext* context, const Layer* old_layer) { @@ -57,16 +58,20 @@ void ClipRectLayer::Paint(PaintContext& context) const { context.internal_nodes_canvas->clipRect(clip_rect_, clip_behavior_ != Clip::hardEdge); - if (UsesSaveLayer()) { - TRACE_EVENT0("flutter", "Canvas::saveLayer"); - context.internal_nodes_canvas->saveLayer(clip_rect_, nullptr); + if (!UsesSaveLayer()) { + PaintChildren(context); + return; } + + AutoCachePaint cache_paint(context); + TRACE_EVENT0("flutter", "Canvas::saveLayer"); + context.internal_nodes_canvas->saveLayer(clip_rect_, cache_paint.paint()); + PaintChildren(context); - if (UsesSaveLayer()) { - context.internal_nodes_canvas->restore(); - if (context.checkerboard_offscreen_layers) { - DrawCheckerboard(context.internal_nodes_canvas, clip_rect_); - } + + context.internal_nodes_canvas->restore(); + if (context.checkerboard_offscreen_layers) { + DrawCheckerboard(context.internal_nodes_canvas, clip_rect_); } } diff --git a/engine/src/flutter/flow/layers/clip_rrect_layer.cc b/engine/src/flutter/flow/layers/clip_rrect_layer.cc index 511b3da5526..52377ae9845 100644 --- a/engine/src/flutter/flow/layers/clip_rrect_layer.cc +++ b/engine/src/flutter/flow/layers/clip_rrect_layer.cc @@ -10,6 +10,7 @@ namespace flutter { ClipRRectLayer::ClipRRectLayer(const SkRRect& clip_rrect, Clip clip_behavior) : clip_rrect_(clip_rrect), clip_behavior_(clip_behavior) { FML_DCHECK(clip_behavior != Clip::none); + set_layer_can_inherit_opacity(true); } void ClipRRectLayer::Diff(DiffContext* context, const Layer* old_layer) { @@ -58,16 +59,20 @@ void ClipRRectLayer::Paint(PaintContext& context) const { context.internal_nodes_canvas->clipRRect(clip_rrect_, clip_behavior_ != Clip::hardEdge); - if (UsesSaveLayer()) { - TRACE_EVENT0("flutter", "Canvas::saveLayer"); - context.internal_nodes_canvas->saveLayer(paint_bounds(), nullptr); + if (!UsesSaveLayer()) { + PaintChildren(context); + return; } + + AutoCachePaint cache_paint(context); + TRACE_EVENT0("flutter", "Canvas::saveLayer"); + context.internal_nodes_canvas->saveLayer(paint_bounds(), cache_paint.paint()); + PaintChildren(context); - if (UsesSaveLayer()) { - context.internal_nodes_canvas->restore(); - if (context.checkerboard_offscreen_layers) { - DrawCheckerboard(context.internal_nodes_canvas, paint_bounds()); - } + + context.internal_nodes_canvas->restore(); + if (context.checkerboard_offscreen_layers) { + DrawCheckerboard(context.internal_nodes_canvas, paint_bounds()); } } diff --git a/engine/src/flutter/flow/layers/container_layer.cc b/engine/src/flutter/flow/layers/container_layer.cc index 552d413f26f..6035ef56e83 100644 --- a/engine/src/flutter/flow/layers/container_layer.cc +++ b/engine/src/flutter/flow/layers/container_layer.cc @@ -121,6 +121,13 @@ void ContainerLayer::Paint(PaintContext& context) const { PaintChildren(context); } +static bool safe_intersection_test(const SkRect* rect1, const SkRect& rect2) { + if (rect1->isEmpty() || rect2.isEmpty()) { + return false; + } + return rect1->intersects(rect2); +} + void ContainerLayer::PrerollChildren(PrerollContext* context, const SkMatrix& child_matrix, SkRect* child_paint_bounds) { @@ -129,13 +136,28 @@ void ContainerLayer::PrerollChildren(PrerollContext* context, FML_DCHECK(!context->has_platform_view); bool child_has_platform_view = false; bool child_has_texture_layer = false; + bool subtree_can_inherit_opacity = layer_can_inherit_opacity(); + for (auto& layer : layers_) { // Reset context->has_platform_view to false so that layers aren't treated // as if they have a platform view based on one being previously found in a // sibling tree. context->has_platform_view = false; + // Initialize the "inherit opacity" flag to the value recorded in the layer + // and allow it to override the answer during its |Preroll| + context->subtree_can_inherit_opacity = layer->layer_can_inherit_opacity(); layer->Preroll(context, child_matrix); + + subtree_can_inherit_opacity = + subtree_can_inherit_opacity && context->subtree_can_inherit_opacity; + if (subtree_can_inherit_opacity && + safe_intersection_test(child_paint_bounds, layer->paint_bounds())) { + // This will allow inheritance by a linear sequence of non-overlapping + // children, but will fail with a grid or other arbitrary 2D layout. + // See https://github.com/flutter/flutter/issues/93899 + subtree_can_inherit_opacity = false; + } child_paint_bounds->join(layer->paint_bounds()); child_has_platform_view = @@ -146,6 +168,7 @@ void ContainerLayer::PrerollChildren(PrerollContext* context, context->has_platform_view = child_has_platform_view; context->has_texture_layer = child_has_texture_layer; + context->subtree_can_inherit_opacity = subtree_can_inherit_opacity; set_subtree_has_platform_view(child_has_platform_view); } @@ -189,6 +212,12 @@ MergedContainerLayer::MergedContainerLayer() { // child becomes the cacheable child, but at the potential cost of // not being as stable in the raster cache from frame to frame. ContainerLayer::Add(std::make_shared()); + + // The interposing Container only recurses to its children with no + // additional processing so, by default, it can pass an inherited + // opacity on to its children, subject only to the accumulation + // logic that happens during |PrerollChildren|. + GetChildContainer()->set_layer_can_inherit_opacity(true); } void MergedContainerLayer::DiffChildren(DiffContext* context, diff --git a/engine/src/flutter/flow/layers/display_list_layer.cc b/engine/src/flutter/flow/layers/display_list_layer.cc index 8a9e5626616..66f278c7729 100644 --- a/engine/src/flutter/flow/layers/display_list_layer.cc +++ b/engine/src/flutter/flow/layers/display_list_layer.cc @@ -15,7 +15,12 @@ DisplayListLayer::DisplayListLayer(const SkPoint& offset, : offset_(offset), display_list_(std::move(display_list)), is_complex_(is_complex), - will_change_(will_change) {} + will_change_(will_change) { + if (display_list_.skia_object()) { + set_layer_can_inherit_opacity( + display_list_.skia_object()->can_apply_group_opacity()); + } +} bool DisplayListLayer::IsReplacing(DiffContext* context, const Layer* layer) const { @@ -94,8 +99,10 @@ void DisplayListLayer::Preroll(PrerollContext* context, if (auto* cache = context->raster_cache) { TRACE_EVENT0("flutter", "DisplayListLayer::RasterCache (Preroll)"); if (context->cull_rect.intersects(bounds)) { - cache->Prepare(context, disp_list, is_complex_, will_change_, matrix, - offset_); + if (cache->Prepare(context, disp_list, is_complex_, will_change_, matrix, + offset_)) { + context->subtree_can_inherit_opacity = true; + } } else { // Don't evict raster cache entry during partial repaint cache->Touch(disp_list, matrix); @@ -116,13 +123,17 @@ void DisplayListLayer::Paint(PaintContext& context) const { context.leaf_nodes_canvas->getTotalMatrix())); #endif - if (context.raster_cache && - context.raster_cache->Draw(*display_list(), *context.leaf_nodes_canvas)) { - TRACE_EVENT_INSTANT0("flutter", "raster cache hit"); - return; + if (context.raster_cache) { + AutoCachePaint cache_paint(context); + if (context.raster_cache->Draw(*display_list(), *context.leaf_nodes_canvas, + cache_paint.paint())) { + TRACE_EVENT_INSTANT0("flutter", "raster cache hit"); + return; + } } - display_list()->RenderTo(context.leaf_nodes_canvas); + display_list()->RenderTo(context.leaf_nodes_canvas, + context.inherited_opacity); } } // namespace flutter diff --git a/engine/src/flutter/flow/layers/layer.cc b/engine/src/flutter/flow/layers/layer.cc index 8a34b775ef7..621ae585701 100644 --- a/engine/src/flutter/flow/layers/layer.cc +++ b/engine/src/flutter/flow/layers/layer.cc @@ -13,7 +13,8 @@ Layer::Layer() : paint_bounds_(SkRect::MakeEmpty()), unique_id_(NextUniqueID()), original_layer_id_(unique_id_), - subtree_has_platform_view_(false) {} + subtree_has_platform_view_(false), + layer_can_inherit_opacity_(false) {} Layer::~Layer() = default; diff --git a/engine/src/flutter/flow/layers/layer.h b/engine/src/flutter/flow/layers/layer.h index f62cc3beb98..e3a8f655fe0 100644 --- a/engine/src/flutter/flow/layers/layer.h +++ b/engine/src/flutter/flow/layers/layer.h @@ -61,6 +61,25 @@ struct PrerollContext { // These allow us to track properties like elevation, opacity, and the // prescence of a texture layer during Preroll. bool has_texture_layer = false; + + // This value indicates that the entire subtree below the layer can inherit + // an opacity value and modulate its own visibility accordingly. + // For Layers which cannot either apply such an inherited opacity nor pass + // it along to their children, they can ignore this value as its default + // behavior is "opt-in". + // For Layers that support this condition, it can be recorded in their + // constructor using the |set_layer_can_inherit_opacity| method and the + // value will be accumulated and recorded by the |PrerollChidren| method + // automatically. + // If the property is more dynamic then the Layer can dynamically set this + // flag before returning from the |Preroll| method. + // For ContainerLayers that need to know if their children can inherit + // the value, the |PrerollChildren| method will have set this value in + // the context before it returns. If the container can support it as long + // as the subtree can support it, no further work needs to be done other + // than to remember the value so that it can choose the right strategy + // for its |Paint| method. + bool subtree_can_inherit_opacity = false; }; class PictureLayer; @@ -147,6 +166,34 @@ class Layer { const RasterCache* raster_cache; const bool checkerboard_offscreen_layers; const float frame_device_pixel_ratio; + + // The following value should be used to modulate the opacity of the + // layer during |Paint|. If the layer does not set the corresponding + // |layer_can_inherit_opacity()| flag, then this value should always + // be |SK_Scalar1|. The value is to be applied as if by using a + // |saveLayer| with an |SkPaint| initialized to this alphaf value and + // a |kSrcOver| blend mode. + SkScalar inherited_opacity = SK_Scalar1; + }; + + class AutoCachePaint { + public: + AutoCachePaint(PaintContext& context) : context_(context) { + needs_paint_ = context.inherited_opacity < SK_Scalar1; + if (needs_paint_) { + paint_.setAlphaf(context.inherited_opacity); + context.inherited_opacity = SK_Scalar1; + } + } + + ~AutoCachePaint() { context_.inherited_opacity = paint_.getAlphaf(); } + + const SkPaint* paint() { return needs_paint_ ? &paint_ : nullptr; } + + private: + PaintContext& context_; + SkPaint paint_; + bool needs_paint_; }; // Calls SkCanvas::saveLayer and restores the layer upon destruction. Also @@ -216,6 +263,18 @@ class Layer { subtree_has_platform_view_ = value; } + // Returns true if the layer can render with an added opacity value inherited + // from an OpacityLayer ancestor and delivered to its |Paint| method through + // the |PaintContext.inherited_opacity| field. This flag can be set either + // in the Layer's constructor if it is a lifetime constant value, or during + // the |Preroll| method if it must determine the capability based on data + // only available when it is part of a tree. It must set this value before + // recursing to its children if it is a |ContainerLayer|. + bool layer_can_inherit_opacity() const { return layer_can_inherit_opacity_; } + void set_layer_can_inherit_opacity(bool value) { + layer_can_inherit_opacity_ = value; + } + // Returns the paint bounds in the layer's local coordinate system // as determined during Preroll(). The bounds should include any // transform, clip or distortions performed by the layer itself, @@ -253,6 +312,9 @@ class Layer { // See https://github.com/flutter/flutter/issues/81419 return true; } + if (context.inherited_opacity == 0) { + return false; + } // Workaround for Skia bug (quickReject does not reject empty bounds). // https://bugs.chromium.org/p/skia/issues/detail?id=10951 if (paint_bounds_.isEmpty()) { @@ -282,6 +344,7 @@ class Layer { uint64_t unique_id_; uint64_t original_layer_id_; bool subtree_has_platform_view_; + bool layer_can_inherit_opacity_; static uint64_t NextUniqueID(); diff --git a/engine/src/flutter/flow/layers/opacity_layer.cc b/engine/src/flutter/flow/layers/opacity_layer.cc index 022dea79592..0eb47622cd8 100644 --- a/engine/src/flutter/flow/layers/opacity_layer.cc +++ b/engine/src/flutter/flow/layers/opacity_layer.cc @@ -10,7 +10,12 @@ namespace flutter { OpacityLayer::OpacityLayer(SkAlpha alpha, const SkPoint& offset) - : alpha_(alpha), offset_(offset) {} + : alpha_(alpha), offset_(offset), children_can_accept_opacity_(false) { + // We can always inhert opacity even if we cannot pass it along to + // our children as we can accumulate the inherited opacity into our + // own opacity value before we recurse. + set_layer_can_inherit_opacity(true); +} void OpacityLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); @@ -50,8 +55,11 @@ void OpacityLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { context->mutators_stack.Pop(); context->mutators_stack.Pop(); - { - set_paint_bounds(paint_bounds().makeOffset(offset_.fX, offset_.fY)); + set_children_can_accept_opacity(context->subtree_can_inherit_opacity); + + set_paint_bounds(paint_bounds().makeOffset(offset_.fX, offset_.fY)); + + if (!children_can_accept_opacity()) { #ifndef SUPPORT_FRACTIONAL_TRANSLATION child_matrix = RasterCache::GetIntegralTransCTM(child_matrix); #endif @@ -66,9 +74,6 @@ void OpacityLayer::Paint(PaintContext& context) const { TRACE_EVENT0("flutter", "OpacityLayer::Paint"); FML_DCHECK(needs_painting(context)); - SkPaint paint; - paint.setAlpha(alpha_); - SkAutoCanvasRestore save(context.internal_nodes_canvas, true); context.internal_nodes_canvas->translate(offset_.fX, offset_.fY); @@ -77,6 +82,19 @@ void OpacityLayer::Paint(PaintContext& context) const { context.leaf_nodes_canvas->getTotalMatrix())); #endif + SkScalar inherited_opacity = context.inherited_opacity; + SkScalar subtree_opacity = opacity() * inherited_opacity; + + if (children_can_accept_opacity()) { + context.inherited_opacity = subtree_opacity; + PaintChildren(context); + context.inherited_opacity = inherited_opacity; + return; + } + + SkPaint paint; + paint.setAlphaf(subtree_opacity); + if (context.raster_cache && context.raster_cache->Draw(GetCacheableChild(), *context.leaf_nodes_canvas, &paint)) { diff --git a/engine/src/flutter/flow/layers/opacity_layer.h b/engine/src/flutter/flow/layers/opacity_layer.h index b3ad2392c45..512d33c3f32 100644 --- a/engine/src/flutter/flow/layers/opacity_layer.h +++ b/engine/src/flutter/flow/layers/opacity_layer.h @@ -33,9 +33,22 @@ class OpacityLayer : public MergedContainerLayer { void Paint(PaintContext& context) const override; + // Returns whether the children are capable of inheriting an opacity value + // and modifying their rendering accordingly. This value is only guaranteed + // to be valid after the local |Preroll| method is called. + bool children_can_accept_opacity() const { + return children_can_accept_opacity_; + } + void set_children_can_accept_opacity(bool value) { + children_can_accept_opacity_ = value; + } + + SkScalar opacity() const { return alpha_ * 1.0 / SK_AlphaOPAQUE; } + private: SkAlpha alpha_; SkPoint offset_; + bool children_can_accept_opacity_; FML_DISALLOW_COPY_AND_ASSIGN(OpacityLayer); }; diff --git a/engine/src/flutter/flow/layers/opacity_layer_unittests.cc b/engine/src/flutter/flow/layers/opacity_layer_unittests.cc index 5fa66904250..acb837cf775 100644 --- a/engine/src/flutter/flow/layers/opacity_layer_unittests.cc +++ b/engine/src/flutter/flow/layers/opacity_layer_unittests.cc @@ -262,8 +262,8 @@ TEST_F(OpacityLayerTest, HalfTransparent) { EXPECT_EQ(mock_layer->parent_mutators(), std::vector({Mutator(layer_transform), Mutator(alpha_half)})); - const SkPaint opacity_paint = - SkPaint(SkColor4f::FromColor(SkColorSetA(SK_ColorBLACK, alpha_half))); + SkPaint opacity_paint; + opacity_paint.setAlphaf(alpha_half * (1.0 / SK_AlphaOPAQUE)); SkRect opacity_bounds; expected_layer_bounds.makeOffset(-layer_offset.fX, -layer_offset.fY) .roundOut(&opacity_bounds); @@ -352,10 +352,10 @@ TEST_F(OpacityLayerTest, Nested) { // EXPECT_EQ(mock_layer3->parent_mutators(), // std::vector({Mutator(layer1_transform), Mutator(alpha1)})); - const SkPaint opacity1_paint = - SkPaint(SkColor4f::FromColor(SkColorSetA(SK_ColorBLACK, alpha1))); - const SkPaint opacity2_paint = - SkPaint(SkColor4f::FromColor(SkColorSetA(SK_ColorBLACK, alpha2))); + SkPaint opacity1_paint; + opacity1_paint.setAlphaf(alpha1 * (1.0 / SK_AlphaOPAQUE)); + SkPaint opacity2_paint; + opacity2_paint.setAlphaf(alpha2 * (1.0 / SK_AlphaOPAQUE)); SkRect opacity1_bounds, opacity2_bounds; expected_layer1_bounds.makeOffset(-layer1_offset.fX, -layer1_offset.fY) .roundOut(&opacity1_bounds); diff --git a/engine/src/flutter/flow/layers/picture_layer.cc b/engine/src/flutter/flow/layers/picture_layer.cc index 4103aadae0a..f93437d993b 100644 --- a/engine/src/flutter/flow/layers/picture_layer.cc +++ b/engine/src/flutter/flow/layers/picture_layer.cc @@ -117,8 +117,10 @@ void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { if (auto* cache = context->raster_cache) { TRACE_EVENT0("flutter", "PictureLayer::RasterCache (Preroll)"); if (context->cull_rect.intersects(bounds)) { - cache->Prepare(context, sk_picture, is_complex_, will_change_, matrix, - offset_); + if (cache->Prepare(context, sk_picture, is_complex_, will_change_, matrix, + offset_)) { + context->subtree_can_inherit_opacity = true; + } } else { // Don't evict raster cache entry during partial repaint cache->Touch(sk_picture, matrix); @@ -140,11 +142,16 @@ void PictureLayer::Paint(PaintContext& context) const { context.leaf_nodes_canvas->getTotalMatrix())); #endif - if (context.raster_cache && - context.raster_cache->Draw(*picture(), *context.leaf_nodes_canvas)) { - TRACE_EVENT_INSTANT0("flutter", "raster cache hit"); - return; + if (context.raster_cache) { + AutoCachePaint cache_paint(context); + if (context.raster_cache->Draw(*picture(), *context.leaf_nodes_canvas, + cache_paint.paint())) { + TRACE_EVENT_INSTANT0("flutter", "raster cache hit"); + return; + } } + + FML_DCHECK(context.inherited_opacity == SK_Scalar1); picture()->playback(context.leaf_nodes_canvas); } diff --git a/engine/src/flutter/flow/layers/shader_mask_layer.cc b/engine/src/flutter/flow/layers/shader_mask_layer.cc index e007ede2145..521029f06ef 100644 --- a/engine/src/flutter/flow/layers/shader_mask_layer.cc +++ b/engine/src/flutter/flow/layers/shader_mask_layer.cc @@ -9,7 +9,9 @@ namespace flutter { ShaderMaskLayer::ShaderMaskLayer(sk_sp shader, const SkRect& mask_rect, SkBlendMode blend_mode) - : shader_(shader), mask_rect_(mask_rect), blend_mode_(blend_mode) {} + : shader_(shader), mask_rect_(mask_rect), blend_mode_(blend_mode) { + set_layer_can_inherit_opacity(true); +} void ShaderMaskLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); @@ -37,8 +39,9 @@ void ShaderMaskLayer::Paint(PaintContext& context) const { TRACE_EVENT0("flutter", "ShaderMaskLayer::Paint"); FML_DCHECK(needs_painting(context)); - Layer::AutoSaveLayer save = - Layer::AutoSaveLayer::Create(context, paint_bounds(), nullptr); + AutoCachePaint cache_paint(context); + Layer::AutoSaveLayer save = Layer::AutoSaveLayer::Create( + context, paint_bounds(), cache_paint.paint()); PaintChildren(context); SkPaint paint; diff --git a/engine/src/flutter/flow/layers/texture_layer.cc b/engine/src/flutter/flow/layers/texture_layer.cc index e360b50d554..63194b0cd5c 100644 --- a/engine/src/flutter/flow/layers/texture_layer.cc +++ b/engine/src/flutter/flow/layers/texture_layer.cc @@ -17,7 +17,9 @@ TextureLayer::TextureLayer(const SkPoint& offset, size_(size), texture_id_(texture_id), freeze_(freeze), - sampling_(sampling) {} + sampling_(sampling) { + set_layer_can_inherit_opacity(true); +} void TextureLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); @@ -58,8 +60,9 @@ void TextureLayer::Paint(PaintContext& context) const { TRACE_EVENT_INSTANT0("flutter", "null texture"); return; } + AutoCachePaint cache_paint(context); texture->Paint(*context.leaf_nodes_canvas, paint_bounds(), freeze_, - context.gr_context, sampling_); + context.gr_context, sampling_, cache_paint.paint()); } } // namespace flutter diff --git a/engine/src/flutter/flow/layers/transform_layer.cc b/engine/src/flutter/flow/layers/transform_layer.cc index 60b1737add8..68cf4e8e319 100644 --- a/engine/src/flutter/flow/layers/transform_layer.cc +++ b/engine/src/flutter/flow/layers/transform_layer.cc @@ -24,6 +24,7 @@ TransformLayer::TransformLayer(const SkMatrix& transform) FML_LOG(ERROR) << "TransformLayer is constructed with an invalid matrix."; transform_.setIdentity(); } + set_layer_can_inherit_opacity(true); } void TransformLayer::Diff(DiffContext* context, const Layer* old_layer) { diff --git a/engine/src/flutter/flow/raster_cache.cc b/engine/src/flutter/flow/raster_cache.cc index 110974b4bca..0d95c17af5b 100644 --- a/engine/src/flutter/flow/raster_cache.cc +++ b/engine/src/flutter/flow/raster_cache.cc @@ -341,7 +341,9 @@ void RasterCache::Touch(DisplayList* display_list, } } -bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const { +bool RasterCache::Draw(const SkPicture& picture, + SkCanvas& canvas, + const SkPaint* paint) const { PictureRasterCacheKey cache_key(picture.uniqueID(), canvas.getTotalMatrix()); auto it = picture_cache_.find(cache_key); if (it == picture_cache_.end()) { @@ -353,7 +355,7 @@ bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const { entry.used_this_frame = true; if (entry.image) { - entry.image->draw(canvas, nullptr); + entry.image->draw(canvas, paint); return true; } @@ -361,7 +363,8 @@ bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const { } bool RasterCache::Draw(const DisplayList& display_list, - SkCanvas& canvas) const { + SkCanvas& canvas, + const SkPaint* paint) const { DisplayListRasterCacheKey cache_key(display_list.unique_id(), canvas.getTotalMatrix()); auto it = display_list_cache_.find(cache_key); @@ -374,7 +377,7 @@ bool RasterCache::Draw(const DisplayList& display_list, entry.used_this_frame = true; if (entry.image) { - entry.image->draw(canvas, nullptr); + entry.image->draw(canvas, paint); return true; } @@ -383,7 +386,7 @@ bool RasterCache::Draw(const DisplayList& display_list, bool RasterCache::Draw(const Layer* layer, SkCanvas& canvas, - SkPaint* paint) const { + const SkPaint* paint) const { LayerRasterCacheKey cache_key(layer->unique_id(), canvas.getTotalMatrix()); auto it = layer_cache_.find(cache_key); if (it == layer_cache_.end()) { diff --git a/engine/src/flutter/flow/raster_cache.h b/engine/src/flutter/flow/raster_cache.h index 2dc052bab67..bcc88cbec88 100644 --- a/engine/src/flutter/flow/raster_cache.h +++ b/engine/src/flutter/flow/raster_cache.h @@ -205,12 +205,16 @@ class RasterCache { // Find the raster cache for the picture and draw it to the canvas. // // Return true if it's found and drawn. - bool Draw(const SkPicture& picture, SkCanvas& canvas) const; + bool Draw(const SkPicture& picture, + SkCanvas& canvas, + const SkPaint* paint = nullptr) const; // Find the raster cache for the display list and draw it to the canvas. // // Return true if it's found and drawn. - bool Draw(const DisplayList& display_list, SkCanvas& canvas) const; + bool Draw(const DisplayList& display_list, + SkCanvas& canvas, + const SkPaint* paint = nullptr) const; // Find the raster cache for the layer and draw it to the canvas. // @@ -220,7 +224,7 @@ class RasterCache { // Return true if the layer raster cache is found and drawn. bool Draw(const Layer* layer, SkCanvas& canvas, - SkPaint* paint = nullptr) const; + const SkPaint* paint = nullptr) const; void PrepareNewFrame(); void CleanupAfterFrame(); diff --git a/engine/src/flutter/flow/testing/mock_texture.cc b/engine/src/flutter/flow/testing/mock_texture.cc index cb5b92c8caa..6808bbf6fcc 100644 --- a/engine/src/flutter/flow/testing/mock_texture.cc +++ b/engine/src/flutter/flow/testing/mock_texture.cc @@ -13,21 +13,22 @@ void MockTexture::Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) { + const SkSamplingOptions& sampling, + const SkPaint* paint) { paint_calls_.emplace_back( - PaintCall{canvas, bounds, freeze, context, sampling}); + PaintCall{canvas, bounds, freeze, context, sampling, paint}); } bool operator==(const MockTexture::PaintCall& a, const MockTexture::PaintCall& b) { return &a.canvas == &b.canvas && a.bounds == b.bounds && a.context == b.context && a.freeze == b.freeze && - a.sampling == b.sampling; + a.sampling == b.sampling && a.paint == b.paint; } std::ostream& operator<<(std::ostream& os, const MockTexture::PaintCall& data) { return os << &data.canvas << " " << data.bounds << " " << data.context << " " - << data.freeze << " " << data.sampling; + << data.freeze << " " << data.sampling << " " << data.paint; } } // namespace testing diff --git a/engine/src/flutter/flow/testing/mock_texture.h b/engine/src/flutter/flow/testing/mock_texture.h index 09ce4c65422..ee4ba6d460a 100644 --- a/engine/src/flutter/flow/testing/mock_texture.h +++ b/engine/src/flutter/flow/testing/mock_texture.h @@ -22,6 +22,7 @@ class MockTexture : public Texture { bool freeze; GrDirectContext* context; SkSamplingOptions sampling; + const SkPaint* paint; }; explicit MockTexture(int64_t textureId); @@ -31,7 +32,8 @@ class MockTexture : public Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) override; + const SkSamplingOptions& sampling, + const SkPaint* paint = nullptr) override; void OnGrContextCreated() override { gr_context_created_ = true; } void OnGrContextDestroyed() override { gr_context_destroyed_ = true; } diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index 409ee2c327e..eb57ea7eaa9 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -1719,7 +1719,8 @@ class MockTexture : public Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions&) override {} + const SkSamplingOptions&, + const SkPaint* paint) override {} void OnGrContextCreated() override {} diff --git a/engine/src/flutter/shell/platform/android/android_external_texture_gl.cc b/engine/src/flutter/shell/platform/android/android_external_texture_gl.cc index 3dd85946b4c..1dcaf848fa6 100644 --- a/engine/src/flutter/shell/platform/android/android_external_texture_gl.cc +++ b/engine/src/flutter/shell/platform/android/android_external_texture_gl.cc @@ -38,7 +38,8 @@ void AndroidExternalTextureGL::Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) { + const SkSamplingOptions& sampling, + const SkPaint* paint) { if (state_ == AttachmentState::detached) { return; } @@ -69,7 +70,7 @@ void AndroidExternalTextureGL::Paint(SkCanvas& canvas, transformAroundCenter.postTranslate(0.5, 0.5); canvas.concat(transformAroundCenter); } - canvas.drawImage(image, 0, 0, sampling, nullptr); + canvas.drawImage(image, 0, 0, sampling, paint); } } diff --git a/engine/src/flutter/shell/platform/android/android_external_texture_gl.h b/engine/src/flutter/shell/platform/android/android_external_texture_gl.h index cbf24f11490..c9fc07fe690 100644 --- a/engine/src/flutter/shell/platform/android/android_external_texture_gl.h +++ b/engine/src/flutter/shell/platform/android/android_external_texture_gl.h @@ -25,7 +25,8 @@ class AndroidExternalTextureGL : public flutter::Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) override; + const SkSamplingOptions& sampling, + const SkPaint* paint) override; void OnGrContextCreated() override; diff --git a/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.h b/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.h index beadd1dfea0..042aec510ab 100644 --- a/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.h +++ b/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.h @@ -30,11 +30,12 @@ textureID:(int64_t)textureID texture:(nonnull NSObject*)texture; -- (void)paint:(SkCanvas&)canvas - bounds:(const SkRect&)bounds - freeze:(BOOL)freeze - grContext:(nonnull GrDirectContext*)grContext - sampling:(const SkSamplingOptions&)sampling; +- (void)canvas:(SkCanvas&)canvas + bounds:(const SkRect&)bounds + freeze:(BOOL)freeze + grContext:(nonnull GrDirectContext*)grContext + sampling:(const SkSamplingOptions&)sampling + paint:(nullable const SkPaint*)paint; - (void)onGrContextCreated; diff --git a/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.mm b/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.mm index c789b1f3225..8d773a77337 100644 --- a/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.mm +++ b/engine/src/flutter/shell/platform/darwin/graphics/FlutterDarwinExternalTextureMetal.mm @@ -46,11 +46,12 @@ FLUTTER_ASSERT_ARC } } -- (void)paint:(SkCanvas&)canvas - bounds:(const SkRect&)bounds - freeze:(BOOL)freeze - grContext:(nonnull GrDirectContext*)grContext - sampling:(const SkSamplingOptions&)sampling { +- (void)canvas:(SkCanvas&)canvas + bounds:(const SkRect&)bounds + freeze:(BOOL)freeze + grContext:(nonnull GrDirectContext*)grContext + sampling:(const SkSamplingOptions&)sampling + paint:(nullable const SkPaint*)paint { const bool needsUpdatedTexture = (!freeze && _textureFrameAvailable) || !_externalImage; if (needsUpdatedTexture) { @@ -62,7 +63,7 @@ FLUTTER_ASSERT_ARC SkRect::Make(_externalImage->bounds()), // source rect bounds, // destination rect sampling, // sampling - nullptr, // paint + paint, // paint SkCanvas::SrcRectConstraint::kFast_SrcRectConstraint // constraint ); } diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.h b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.h index 9e54dba5d24..5318eb33a55 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.h @@ -37,7 +37,8 @@ class IOSExternalTextureGL final : public Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) override; + const SkSamplingOptions& sampling, + const SkPaint* paint) override; // |Texture| void OnGrContextCreated() override; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.mm index fd3e0d48aa3..e4f0b29947e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_gl.mm @@ -144,7 +144,8 @@ void IOSExternalTextureGL::Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) { + const SkSamplingOptions& sampling, + const SkPaint* paint) { EnsureTextureCacheExists(); if (NeedUpdateTexture(freeze)) { auto pixelBuffer = [external_texture_.get() copyPixelBuffer]; @@ -169,7 +170,7 @@ void IOSExternalTextureGL::Paint(SkCanvas& canvas, FML_DCHECK(image) << "Failed to create SkImage from Texture."; if (image) { - canvas.drawImage(image, bounds.x(), bounds.y(), sampling, nullptr); + canvas.drawImage(image, bounds.x(), bounds.y(), sampling, paint); } } diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.h b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.h index 8aab36c676d..158497d3363 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.h @@ -30,7 +30,8 @@ class IOSExternalTextureMetal final : public Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) override; + const SkSamplingOptions& sampling, + const SkPaint* paint) override; // |Texture| void OnGrContextCreated() override; diff --git a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.mm b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.mm index 52dac7b3a47..50d1405a084 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/ios_external_texture_metal.mm @@ -17,12 +17,14 @@ void IOSExternalTextureMetal::Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) { - [darwin_external_texture_metal_ paint:canvas - bounds:bounds - freeze:freeze - grContext:context - sampling:sampling]; + const SkSamplingOptions& sampling, + const SkPaint* paint) { + [darwin_external_texture_metal_ canvas:canvas + bounds:bounds + freeze:freeze + grContext:context + sampling:sampling + paint:paint]; } void IOSExternalTextureMetal::OnGrContextCreated() { diff --git a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc index 55efc4bbc85..249f8d4f9a9 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc @@ -26,7 +26,8 @@ void EmbedderExternalTextureGL::Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) { + const SkSamplingOptions& sampling, + const SkPaint* paint) { if (last_image_ == nullptr) { last_image_ = ResolveTexture(Id(), // @@ -37,9 +38,9 @@ void EmbedderExternalTextureGL::Paint(SkCanvas& canvas, if (last_image_) { if (bounds != SkRect::Make(last_image_->bounds())) { - canvas.drawImageRect(last_image_, bounds, sampling); + canvas.drawImageRect(last_image_, bounds, sampling, paint); } else { - canvas.drawImage(last_image_, bounds.x(), bounds.y(), sampling, nullptr); + canvas.drawImage(last_image_, bounds.x(), bounds.y(), sampling, paint); } } } diff --git a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.h b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.h index f1373405aec..887a0e50fe0 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.h +++ b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.h @@ -36,7 +36,8 @@ class EmbedderExternalTextureGL : public flutter::Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) override; + const SkSamplingOptions& sampling, + const SkPaint* paint) override; // |flutter::Texture| void OnGrContextCreated() override; diff --git a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.h b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.h index fb34a13eade..0587aa0f69e 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.h +++ b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.h @@ -36,7 +36,8 @@ class EmbedderExternalTextureMetal : public flutter::Texture { const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) override; + const SkSamplingOptions& sampling, + const SkPaint* paint) override; // |flutter::Texture| void OnGrContextCreated() override; diff --git a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.mm b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.mm index c2ac41f6cb7..4c880dc8b8a 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.mm +++ b/engine/src/flutter/shell/platform/embedder/embedder_external_texture_metal.mm @@ -35,16 +35,17 @@ void EmbedderExternalTextureMetal::Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrDirectContext* context, - const SkSamplingOptions& sampling) { + const SkSamplingOptions& sampling, + const SkPaint* paint) { if (last_image_ == nullptr) { last_image_ = ResolveTexture(Id(), context, SkISize::Make(bounds.width(), bounds.height())); } if (last_image_) { if (bounds != SkRect::Make(last_image_->bounds())) { - canvas.drawImageRect(last_image_, bounds, sampling); + canvas.drawImageRect(last_image_, bounds, sampling, paint); } else { - canvas.drawImage(last_image_, bounds.x(), bounds.y(), sampling, nullptr); + canvas.drawImage(last_image_, bounds.x(), bounds.y(), sampling, paint); } } } diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart index 5196f377275..39c6a471bff 100644 --- a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart @@ -301,9 +301,11 @@ void null_platform_messages() { Picture CreateSimplePicture() { Paint blackPaint = Paint(); + Paint whitePaint = Paint()..color = Color.fromARGB(255, 255, 255, 255); PictureRecorder baseRecorder = PictureRecorder(); Canvas canvas = Canvas(baseRecorder); canvas.drawRect(Rect.fromLTRB(0.0, 0.0, 1000.0, 1000.0), blackPaint); + canvas.drawRect(Rect.fromLTRB(10.0, 10.0, 990.0, 990.0), whitePaint); return baseRecorder.endRecording(); } diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/verifyb143464703_soft_noxform.png b/engine/src/flutter/shell/platform/embedder/fixtures/verifyb143464703_soft_noxform.png index b89b38290b87c9adfd1b1ae13adbed4b86a577a2..38097bf97b876feffbb3010d5a2a0351e8886a9f 100644 GIT binary patch literal 6981 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 literal 6985 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 TJP-d#0bs+rO>2dp>^$-}t