Skip to content

Commit 3caa7e7

Browse files
authored
Enhance image_filter_layer caching to filter a cached child (flutter#17175)
1 parent 559d93d commit 3caa7e7

File tree

8 files changed

+318
-104
lines changed

8 files changed

+318
-104
lines changed

flow/layers/container_layer.cc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,36 @@ void ContainerLayer::UpdateSceneChildren(SceneUpdateContext& context) {
160160

161161
#endif // defined(OS_FUCHSIA)
162162

163+
MergedContainerLayer::MergedContainerLayer() {
164+
// Ensure the layer has only one direct child.
165+
//
166+
// Any children will actually be added as children of this empty
167+
// ContainerLayer which can be accessed via ::GetContainerLayer().
168+
// If only one child is ever added to this layer then that child
169+
// will become the layer returned from ::GetCacheableChild().
170+
// If multiple child layers are added, then this implicit container
171+
// child becomes the cacheable child, but at the potential cost of
172+
// not being as stable in the raster cache from frame to frame.
173+
ContainerLayer::Add(std::make_shared<ContainerLayer>());
174+
}
175+
176+
void MergedContainerLayer::Add(std::shared_ptr<Layer> layer) {
177+
GetChildContainer()->Add(std::move(layer));
178+
}
179+
180+
ContainerLayer* MergedContainerLayer::GetChildContainer() const {
181+
FML_DCHECK(layers().size() == 1);
182+
183+
return static_cast<ContainerLayer*>(layers()[0].get());
184+
}
185+
186+
Layer* MergedContainerLayer::GetCacheableChild() const {
187+
ContainerLayer* child_container = GetChildContainer();
188+
if (child_container->layers().size() == 1) {
189+
return child_container->layers()[0].get();
190+
}
191+
192+
return child_container;
193+
}
194+
163195
} // namespace flutter

flow/layers/container_layer.h

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ class ContainerLayer : public Layer {
3535
void UpdateSceneChildren(SceneUpdateContext& context);
3636
#endif // defined(OS_FUCHSIA)
3737

38-
// For OpacityLayer to restructure to have a single child.
39-
void ClearChildren() { layers_.clear(); }
40-
4138
// Try to prepare the raster cache for a given layer.
4239
//
4340
// The raster cache would fail if either of the followings is true:
@@ -58,6 +55,81 @@ class ContainerLayer : public Layer {
5855
FML_DISALLOW_COPY_AND_ASSIGN(ContainerLayer);
5956
};
6057

58+
//------------------------------------------------------------------------------
59+
/// Some ContainerLayer objects perform a rendering operation or filter on
60+
/// the rendered output of their children. Often that operation is changed
61+
/// slightly from frame to frame as part of an animation. During such an
62+
/// animation, the children can be cached if they are stable to avoid having
63+
/// to render them on every frame. Even if the children are not stable,
64+
/// rendering them into the raster cache during a Preroll operation will save
65+
/// an extra change of rendering surface during the Paint phase as compared
66+
/// to using the SaveLayer that would otherwise be needed with no caching.
67+
///
68+
/// Typically the Flutter Widget objects that lead to the creation of these
69+
/// layers will try to enforce only a single child Widget by their design.
70+
/// Unfortunately, the process of turning Widgets eventually into engine
71+
/// layers is not a 1:1 process so this layer might end up with multiple
72+
/// child layers even if the Widget only had a single child Widget.
73+
///
74+
/// When such a layer goes to cache the output of its children, it will
75+
/// need to supply a single layer to the cache mechanism since the raster
76+
/// cache uses a layer unique_id() as part of the cache key. If this layer
77+
/// ended up with multiple children, then it must first collect them into
78+
/// one layer for the cache mechanism. In order to provide a single layer
79+
/// for all of the children, this utility class will implicitly collect
80+
/// the children into a secondary ContainerLayer called the child container.
81+
///
82+
/// A by-product of creating a hidden child container, though, is that the
83+
/// child container is created new every time this layer is created with
84+
/// different properties, such as during an animation. In that scenario,
85+
/// it would be best to cache the single real child of this layer if it
86+
/// is unique and if it is stable from frame to frame. To facilitate this
87+
/// optimal caching strategy, this class implements two accessor methods
88+
/// to be used for different purposes:
89+
///
90+
/// When the layer needs to recurse to perform some operation on its children,
91+
/// it can call GetChildContainer() to return the hidden container containing
92+
/// all of the real children.
93+
///
94+
/// When the layer wants to cache the rendered contents of its children, it
95+
/// should call GetCacheableChild() for best performance. This method may
96+
/// end up returning the same layer as GetChildContainer(), but only if the
97+
/// conditions for optimal caching of a single child are not met.
98+
///
99+
class MergedContainerLayer : public ContainerLayer {
100+
public:
101+
MergedContainerLayer();
102+
103+
void Add(std::shared_ptr<Layer> layer) override;
104+
105+
protected:
106+
/**
107+
* @brief Returns the ContainerLayer used to hold all of the children of the
108+
* MergedContainerLayer. Note that this may not be the best layer to use
109+
* for caching the children.
110+
*
111+
* @see GetCacheableChild()
112+
* @return the ContainerLayer child used to hold the children
113+
*/
114+
ContainerLayer* GetChildContainer() const;
115+
116+
/**
117+
* @brief Returns the best choice for a Layer object that can be used
118+
* in RasterCache operations to cache the children.
119+
*
120+
* The returned Layer must represent all children and try to remain stable
121+
* if the MergedContainerLayer is reconstructed in subsequent frames of
122+
* the scene.
123+
*
124+
* @see GetChildContainer()
125+
* @return the best candidate Layer for caching the children
126+
*/
127+
Layer* GetCacheableChild() const;
128+
129+
private:
130+
FML_DISALLOW_COPY_AND_ASSIGN(MergedContainerLayer);
131+
};
132+
61133
} // namespace flutter
62134

63135
#endif // FLUTTER_FLOW_LAYERS_CONTAINER_LAYER_H_

flow/layers/container_layer_unittests.cc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,5 +198,75 @@ TEST_F(ContainerLayerTest, NeedsSystemComposite) {
198198
child_path2, child_paint2}}}));
199199
}
200200

201+
TEST_F(ContainerLayerTest, MergedOneChild) {
202+
SkPath child_path;
203+
child_path.addRect(5.0f, 6.0f, 20.5f, 21.5f);
204+
SkPaint child_paint(SkColors::kGreen);
205+
SkMatrix initial_transform = SkMatrix::Translate(-0.5f, -0.5f);
206+
207+
auto mock_layer = std::make_shared<MockLayer>(child_path, child_paint);
208+
auto layer = std::make_shared<MergedContainerLayer>();
209+
layer->Add(mock_layer);
210+
211+
layer->Preroll(preroll_context(), initial_transform);
212+
EXPECT_FALSE(preroll_context()->has_platform_view);
213+
EXPECT_EQ(mock_layer->paint_bounds(), child_path.getBounds());
214+
EXPECT_EQ(layer->paint_bounds(), child_path.getBounds());
215+
EXPECT_TRUE(mock_layer->needs_painting());
216+
EXPECT_TRUE(layer->needs_painting());
217+
EXPECT_FALSE(mock_layer->needs_system_composite());
218+
EXPECT_FALSE(layer->needs_system_composite());
219+
EXPECT_EQ(mock_layer->parent_matrix(), initial_transform);
220+
EXPECT_EQ(mock_layer->parent_cull_rect(), kGiantRect);
221+
222+
layer->Paint(paint_context());
223+
EXPECT_EQ(mock_canvas().draw_calls(),
224+
std::vector({MockCanvas::DrawCall{
225+
0, MockCanvas::DrawPathData{child_path, child_paint}}}));
226+
}
227+
228+
TEST_F(ContainerLayerTest, MergedMultipleChildren) {
229+
SkPath child_path1;
230+
child_path1.addRect(5.0f, 6.0f, 20.5f, 21.5f);
231+
SkPath child_path2;
232+
child_path2.addRect(58.0f, 2.0f, 16.5f, 14.5f);
233+
SkPaint child_paint1(SkColors::kGray);
234+
SkPaint child_paint2(SkColors::kGreen);
235+
SkMatrix initial_transform = SkMatrix::Translate(-0.5f, -0.5f);
236+
237+
auto mock_layer1 = std::make_shared<MockLayer>(child_path1, child_paint1);
238+
auto mock_layer2 = std::make_shared<MockLayer>(child_path2, child_paint2);
239+
auto layer = std::make_shared<MergedContainerLayer>();
240+
layer->Add(mock_layer1);
241+
layer->Add(mock_layer2);
242+
243+
SkRect expected_total_bounds = child_path1.getBounds();
244+
expected_total_bounds.join(child_path2.getBounds());
245+
layer->Preroll(preroll_context(), initial_transform);
246+
EXPECT_FALSE(preroll_context()->has_platform_view);
247+
EXPECT_EQ(mock_layer1->paint_bounds(), child_path1.getBounds());
248+
EXPECT_EQ(mock_layer2->paint_bounds(), child_path2.getBounds());
249+
EXPECT_EQ(layer->paint_bounds(), expected_total_bounds);
250+
EXPECT_TRUE(mock_layer1->needs_painting());
251+
EXPECT_TRUE(mock_layer2->needs_painting());
252+
EXPECT_TRUE(layer->needs_painting());
253+
EXPECT_FALSE(mock_layer1->needs_system_composite());
254+
EXPECT_FALSE(mock_layer2->needs_system_composite());
255+
EXPECT_FALSE(layer->needs_system_composite());
256+
EXPECT_EQ(mock_layer1->parent_matrix(), initial_transform);
257+
EXPECT_EQ(mock_layer2->parent_matrix(), initial_transform);
258+
EXPECT_EQ(mock_layer1->parent_cull_rect(), kGiantRect);
259+
EXPECT_EQ(mock_layer2->parent_cull_rect(),
260+
kGiantRect); // Siblings are independent
261+
262+
layer->Paint(paint_context());
263+
EXPECT_EQ(
264+
mock_canvas().draw_calls(),
265+
std::vector({MockCanvas::DrawCall{
266+
0, MockCanvas::DrawPathData{child_path1, child_paint1}},
267+
MockCanvas::DrawCall{0, MockCanvas::DrawPathData{
268+
child_path2, child_paint2}}}));
269+
}
270+
201271
} // namespace testing
202272
} // namespace flutter

flow/layers/image_filter_layer.cc

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
namespace flutter {
88

99
ImageFilterLayer::ImageFilterLayer(sk_sp<SkImageFilter> filter)
10-
: filter_(std::move(filter)) {}
10+
: filter_(std::move(filter)),
11+
transformed_filter_(nullptr),
12+
render_count_(1) {}
1113

1214
void ImageFilterLayer::Preroll(PrerollContext* context,
1315
const SkMatrix& matrix) {
@@ -16,39 +18,77 @@ void ImageFilterLayer::Preroll(PrerollContext* context,
1618
Layer::AutoPrerollSaveLayerState save =
1719
Layer::AutoPrerollSaveLayerState::Create(context);
1820

19-
child_paint_bounds_ = SkRect::MakeEmpty();
20-
PrerollChildren(context, matrix, &child_paint_bounds_);
21+
SkRect child_bounds = SkRect::MakeEmpty();
22+
PrerollChildren(context, matrix, &child_bounds);
2123
if (filter_) {
22-
const SkIRect filter_input_bounds = child_paint_bounds_.roundOut();
24+
const SkIRect filter_input_bounds = child_bounds.roundOut();
2325
SkIRect filter_output_bounds =
2426
filter_->filterBounds(filter_input_bounds, SkMatrix::I(),
2527
SkImageFilter::kForward_MapDirection);
26-
set_paint_bounds(SkRect::Make(filter_output_bounds));
27-
} else {
28-
set_paint_bounds(child_paint_bounds_);
28+
child_bounds = SkRect::Make(filter_output_bounds);
2929
}
30+
set_paint_bounds(child_bounds);
31+
32+
transformed_filter_ = nullptr;
33+
if (render_count_ >= kMinimumRendersBeforeCachingFilterLayer) {
34+
// We have rendered this same ImageFilterLayer object enough
35+
// times to consider its properties and children to be stable
36+
// from frame to frame so we try to cache the layer itself
37+
// for maximum performance.
38+
TryToPrepareRasterCache(context, this, matrix);
39+
} else {
40+
// This ImageFilterLayer is not yet considered stable so we
41+
// increment the count to measure how many times it has been
42+
// seen from frame to frame.
43+
render_count_++;
3044

31-
TryToPrepareRasterCache(context, this, matrix);
45+
// Now we will try to pre-render the children into the cache.
46+
// To apply the filter to pre-rendered children, we must first
47+
// modify the filter to be aware of the transform under which
48+
// the cached bitmap was produced. Some SkImageFilter
49+
// instances can do this operation on some transforms and some
50+
// (filters or transforms) cannot. We can only cache the children
51+
// and apply the filter on the fly if this operation succeeds.
52+
transformed_filter_ = filter_->makeWithLocalMatrix(matrix);
53+
if (transformed_filter_) {
54+
// With a modified SkImageFilter we can now try to cache the
55+
// children to avoid their rendering costs if they remain
56+
// stable between frames and also avoiding a rendering surface
57+
// switch during the Paint phase even if they are not stable.
58+
// This benefit is seen most during animations.
59+
TryToPrepareRasterCache(context, GetCacheableChild(), matrix);
60+
}
61+
}
3262
}
3363

3464
void ImageFilterLayer::Paint(PaintContext& context) const {
3565
TRACE_EVENT0("flutter", "ImageFilterLayer::Paint");
3666
FML_DCHECK(needs_painting());
3767

38-
if (context.raster_cache &&
39-
context.raster_cache->Draw(this, *context.leaf_nodes_canvas)) {
40-
return;
68+
if (context.raster_cache) {
69+
if (context.raster_cache->Draw(this, *context.leaf_nodes_canvas)) {
70+
return;
71+
}
72+
if (transformed_filter_) {
73+
SkPaint paint;
74+
paint.setImageFilter(transformed_filter_);
75+
76+
if (context.raster_cache->Draw(GetCacheableChild(),
77+
*context.leaf_nodes_canvas, &paint)) {
78+
return;
79+
}
80+
}
4181
}
4282

4383
SkPaint paint;
4484
paint.setImageFilter(filter_);
4585

4686
// Normally a save_layer is sized to the current layer bounds, but in this
4787
// case the bounds of the child may not be the same as the filtered version
48-
// so we use the child_paint_bounds_ which were snapshotted from the
49-
// Preroll on the children before we adjusted them based on the filter.
50-
Layer::AutoSaveLayer save_layer =
51-
Layer::AutoSaveLayer::Create(context, child_paint_bounds_, &paint);
88+
// so we use the bounds of the child container which do not include any
89+
// modifications that the filter might apply.
90+
Layer::AutoSaveLayer save_layer = Layer::AutoSaveLayer::Create(
91+
context, GetChildContainer()->paint_bounds(), &paint);
5292
PaintChildren(context);
5393
}
5494

flow/layers/image_filter_layer.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace flutter {
1313

14-
class ImageFilterLayer : public ContainerLayer {
14+
class ImageFilterLayer : public MergedContainerLayer {
1515
public:
1616
ImageFilterLayer(sk_sp<SkImageFilter> filter);
1717

@@ -20,8 +20,25 @@ class ImageFilterLayer : public ContainerLayer {
2020
void Paint(PaintContext& context) const override;
2121

2222
private:
23+
// The ImageFilterLayer might cache the filtered output of this layer
24+
// if the layer remains stable (if it is not animating for instance).
25+
// If the ImageFilterLayer is not the same between rendered frames,
26+
// though, it will cache its children instead and filter their cached
27+
// output on the fly.
28+
// Caching just the children saves the time to render them and also
29+
// avoids a rendering surface switch to draw them.
30+
// Caching the layer itself avoids all of that and additionally avoids
31+
// the cost of applying the filter, but can be worse than caching the
32+
// children if the filter itself is not stable from frame to frame.
33+
// This constant controls how many times we will Preroll and Paint this
34+
// same ImageFilterLayer before we consider the layer and filter to be
35+
// stable enough to switch from caching the children to caching the
36+
// filtered output of this layer.
37+
static constexpr int kMinimumRendersBeforeCachingFilterLayer = 3;
38+
2339
sk_sp<SkImageFilter> filter_;
24-
SkRect child_paint_bounds_;
40+
sk_sp<SkImageFilter> transformed_filter_;
41+
int render_count_;
2542

2643
FML_DISALLOW_COPY_AND_ASSIGN(ImageFilterLayer);
2744
};

0 commit comments

Comments
 (0)