[canvaskit] Improve how overlays are optimized (flutter/engine#54547)

Enhances the overlay optimization by pushing new pictures to the earliest RenderCanvas they can go into. This improvement is made especially clear in the new test case I added to `embedded_views_test.dart`, with the previous algorithm we would have used an overlay for each platform view even though all of the pictures could go into the base canvas with no issue.

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
Harry Terkelsen 2024-08-15 15:23:00 -07:00 committed by GitHub
parent 9c6ec923c9
commit e0529ddd81
2 changed files with 210 additions and 24 deletions

View File

@ -156,55 +156,115 @@ ui.Rect computePlatformViewBounds(EmbeddedViewParams params) {
/// [platformViews].
///
/// [paramsForViews] is required to compute the bounds of the platform views.
// TODO(harryterkelsen): Extend this to work for any sequence of platform views
// and pictures, https://github.com/flutter/flutter/issues/149863.
Rendering createOptimizedRendering(
List<CkPicture> pictures,
List<int> platformViews,
Map<int, EmbeddedViewParams> paramsForViews,
) {
final Map<int, ui.Rect> cachedComputedRects = <int, ui.Rect>{};
assert(pictures.length == platformViews.length + 1);
final Rendering result = Rendering();
// The first render canvas is required due to the pseudo-platform view "V_0"
// which is defined as a platform view that comes before all Flutter drawing
// commands and intersects with everything.
RenderingRenderCanvas currentRenderCanvas = RenderingRenderCanvas();
// This line essentially unwinds the first iteration of the following loop.
// Since "V_0" intersects with all subsequent pictures, then the first picture
// it intersects with is "P_0", so we create a new render canvas and add "P_0"
// to it.
// The first picture is added to the rendering in a new render canvas.
RenderingRenderCanvas tentativeCanvas = RenderingRenderCanvas();
if (!pictures[0].cullRect.isEmpty) {
currentRenderCanvas.add(pictures[0]);
tentativeCanvas.add(pictures[0]);
}
for (int i = 0; i < platformViews.length; i++) {
final RenderingPlatformView platformView =
RenderingPlatformView(platformViews[i]);
if (PlatformViewManager.instance.isVisible(platformViews[i])) {
final ui.Rect platformViewBounds =
final ui.Rect platformViewBounds = cachedComputedRects[platformViews[i]] =
computePlatformViewBounds(paramsForViews[platformViews[i]]!);
if (debugOverlayOptimizationBounds) {
platformView.debugComputedBounds = platformViewBounds;
}
bool intersectsWithCurrentPictures = false;
for (final CkPicture picture in currentRenderCanvas.pictures) {
if (picture.cullRect.overlaps(platformViewBounds)) {
intersectsWithCurrentPictures = true;
// If the platform view intersects with any pictures in the tentative canvas
// then add the tentative canvas to the rendering.
for (final CkPicture picture in tentativeCanvas.pictures) {
if (!picture.cullRect.intersect(platformViewBounds).isEmpty) {
result.add(tentativeCanvas);
tentativeCanvas = RenderingRenderCanvas();
break;
}
}
if (intersectsWithCurrentPictures) {
result.add(currentRenderCanvas);
currentRenderCanvas = RenderingRenderCanvas();
}
}
result.add(platformView);
if (!pictures[i + 1].cullRect.isEmpty) {
currentRenderCanvas.add(pictures[i + 1]);
if (pictures[i + 1].cullRect.isEmpty) {
continue;
}
// Find the first render canvas which comes after the last entity (picture
// or platform view) that the next picture intersects with, and add the
// picture to that render canvas, or create a new render canvas.
// First check if the picture intersects with any pictures in the tentative
// canvas, as this will be the last canvas in the rendering when it is
// eventually added.
bool addedToTentativeCanvas = false;
for (final CkPicture picture in tentativeCanvas.pictures) {
if (!picture.cullRect.intersect(pictures[i + 1].cullRect).isEmpty) {
tentativeCanvas.add(pictures[i + 1]);
addedToTentativeCanvas = true;
break;
}
}
if (addedToTentativeCanvas) {
continue;
}
RenderingRenderCanvas? lastCanvasSeen;
bool addedPictureToRendering = false;
for (final RenderingEntity entity in result.entities.reversed) {
if (entity is RenderingPlatformView) {
if (PlatformViewManager.instance.isVisible(entity.viewId)) {
final ui.Rect platformViewBounds =
cachedComputedRects[entity.viewId]!;
if (!platformViewBounds.intersect(pictures[i + 1].cullRect).isEmpty) {
// The next picture intersects with a platform view already in the
// result. Add this picture to the first render canvas which comes
// after this platform view or create one if none exists.
if (lastCanvasSeen != null) {
lastCanvasSeen.add(pictures[i + 1]);
} else {
tentativeCanvas.add(pictures[i + 1]);
}
addedPictureToRendering = true;
break;
}
}
} else if (entity is RenderingRenderCanvas) {
lastCanvasSeen = entity;
// Check if we intersect with any pictures in this render canvas.
for (final CkPicture picture in entity.pictures) {
if (!picture.cullRect.intersect(pictures[i + 1].cullRect).isEmpty) {
lastCanvasSeen.add(pictures[i + 1]);
addedPictureToRendering = true;
break;
}
}
}
}
if (!addedPictureToRendering) {
if (lastCanvasSeen != null) {
// Add it to the last canvas seen in the rendering, if any.
lastCanvasSeen.add(pictures[i + 1]);
} else {
tentativeCanvas.add(pictures[i + 1]);
}
}
}
if (currentRenderCanvas.pictures.isNotEmpty) {
result.add(currentRenderCanvas);
if (tentativeCanvas.pictures.isNotEmpty) {
result.add(tentativeCanvas);
}
return result;
}

View File

@ -1120,9 +1120,135 @@ void testMain() {
_platformView,
_overlay,
]);
// Scene 4: Same as scene 1 but with a placeholder rectangle painted
// under each platform view. This is closer to how the real Flutter
// framework would render a grid of platform views. Interestingly, in this
// case every drawing can go in a base canvas.
final LayerSceneBuilder sb4 = LayerSceneBuilder();
sb4.pushOffset(0, 0);
sb4.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 300, 300)));
sb4.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(10, 10, 50, 50)));
sb4.addPlatformView(0,
offset: const ui.Offset(10, 10), width: 50, height: 50);
sb4.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(70, 10, 50, 50)));
sb4.addPlatformView(1,
offset: const ui.Offset(70, 10), width: 50, height: 50);
sb4.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(130, 10, 50, 50)));
sb4.addPlatformView(2,
offset: const ui.Offset(130, 10), width: 50, height: 50);
final LayerScene scene4 = sb4.build();
await renderScene(scene4);
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
_platformView,
_platformView,
_platformView,
]);
// Scene 5: A combination of scene 1 and scene 4, where a subtitle is
// painted over each platform view and a placeholder is painted under each
// one. Unfortunately, we need an overlay for each platform view in this
// case.
final LayerSceneBuilder sb5 = LayerSceneBuilder();
sb5.pushOffset(0, 0);
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 300, 300)));
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(10, 10, 50, 50)));
sb5.addPlatformView(0,
offset: const ui.Offset(10, 10), width: 50, height: 50);
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(12, 12, 10, 10)));
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(70, 10, 50, 50)));
sb5.addPlatformView(1,
offset: const ui.Offset(70, 10), width: 50, height: 50);
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(72, 12, 10, 10)));
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(130, 10, 50, 50)));
sb5.addPlatformView(2,
offset: const ui.Offset(130, 10), width: 50, height: 50);
sb5.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(132, 12, 10, 10)));
final LayerScene scene5 = sb5.build();
await renderScene(scene5);
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
_platformView,
_overlay,
_platformView,
_overlay,
_platformView,
_overlay,
]);
});
test('sinks platform view under the canvas if it does not overlap with the picture',
test(
'correctly places pictures in case where next '
'picture intersects multiple elements', () async {
ui_web.platformViewRegistry.registerViewFactory(
'test-view',
(int viewId) => createDomHTMLDivElement()..className = 'platform-view',
);
ui_web.platformViewRegistry.registerViewFactory(
'invisible-view',
(int viewId) =>
createDomHTMLDivElement()..className = 'invisible-platform-view',
isVisible: false,
);
CkPicture rectPicture(ui.Rect rect) {
return paintPicture(rect, (CkCanvas canvas) {
canvas.drawRect(
rect, CkPaint()..color = const ui.Color.fromARGB(255, 255, 0, 0));
});
}
await createPlatformView(0, 'test-view');
await createPlatformView(1, 'invisible-view');
expect(PlatformViewManager.instance.isVisible(0), isTrue);
expect(PlatformViewManager.instance.isVisible(1), isFalse);
final LayerSceneBuilder sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 100, 100)));
sb.addPlatformView(0,
offset: const ui.Offset(10, 10), width: 50, height: 50);
sb.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 100, 100)));
sb.addPlatformView(1,
offset: const ui.Offset(10, 10), width: 50, height: 50);
sb.addPicture(
ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 5, 5)));
final LayerScene scene = sb.build();
await renderScene(scene);
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
_platformView,
_platformView,
_overlay,
]);
final Rendering rendering = CanvasKitRenderer.instance
.debugGetRasterizerForView(implicitView)!
.viewEmbedder
.debugActiveRendering;
final List<int> picturesPerCanvas = rendering.canvases
.map((RenderingRenderCanvas canvas) => canvas.pictures.length)
.toList();
expect(picturesPerCanvas, <int>[1, 2]);
});
test(
'sinks platform view under the canvas if it does not overlap with the picture',
() async {
ui_web.platformViewRegistry.registerViewFactory(
'test-view',