From 31cde4f8d3b4116d27e1bd0a71c480316d6ca5ce Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:39:10 -0700 Subject: [PATCH] [canvaskit] Further improve overlay optimization by splitting pictures (flutter/engine#54878) This enhances the overlay optimization by delaying combining pictures to get tighter bounds for the pictures that make up the scene, enabling more sophisticated optimization since we can determine if they intersect with platform views on a per-picture basis. Fixes https://github.com/flutter/flutter/issues/149863 On a Macbook in Chrome in an example app with an infinite scrolling grid of platform views, this brings the ratio of dropped frames from 93% to 55% (roughly 4 fps to 30 fps). [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../ci/licenses_golden/licenses_flutter | 2 + .../flutter/lib/web_ui/lib/src/engine.dart | 1 + .../src/engine/canvaskit/embedded_views.dart | 189 +++-- .../lib/src/engine/canvaskit/layer.dart | 450 ++---------- .../lib/src/engine/canvaskit/layer_tree.dart | 55 +- .../src/engine/canvaskit/layer_visitor.dart | 680 ++++++++++++++++++ .../canvaskit/overlay_scene_optimizer.dart | 152 ++-- .../lib/src/engine/canvaskit/rasterizer.dart | 9 +- .../test/canvaskit/embedded_views_test.dart | 6 +- .../web_ui/test/ui/scene_builder_test.dart | 18 + 10 files changed, 1003 insertions(+), 559 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index f6cdc665d4..6dae3b2eb7 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -43589,6 +43589,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.da ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart + ../../../flutter/LICENSE @@ -46469,6 +46470,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index cdf615a388..f50b7cf78c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -33,6 +33,7 @@ export 'engine/canvaskit/image_web_codecs.dart'; export 'engine/canvaskit/layer.dart'; export 'engine/canvaskit/layer_scene_builder.dart'; export 'engine/canvaskit/layer_tree.dart'; +export 'engine/canvaskit/layer_visitor.dart'; export 'engine/canvaskit/mask_filter.dart'; export 'engine/canvaskit/multi_surface_rasterizer.dart'; export 'engine/canvaskit/n_way_canvas.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index a99216b763..936f07b349 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -14,6 +14,7 @@ import '../svg.dart'; import '../util.dart'; import '../vector_math.dart'; import 'canvas.dart'; +import 'layer.dart'; import 'overlay_scene_optimizer.dart'; import 'painting.dart'; import 'path.dart'; @@ -66,6 +67,9 @@ class HtmlViewEmbedder { /// Returns the most recent rendering. Only used in tests. Rendering get debugActiveRendering => _activeRendering; + /// If [debugOverlayOptimizationBounds] is true, this canvas will draw + /// semitransparent rectangles showing the computed bounds of the platform + /// views and pictures in the scene. DisplayCanvas? debugBoundsCanvas; /// The size of the frame, in physical pixels. @@ -75,27 +79,23 @@ class HtmlViewEmbedder { _frameSize = size; } - /// Returns a list of canvases which will be overlaid on top of the "base" - /// canvas after a platform view is composited into the scene. - /// - /// The engine asks for the overlay canvases immediately before the paint - /// phase, after the preroll phase. In the preroll phase we must be - /// conservative and assume that every platform view which is prerolled is - /// also composited, and therefore requires an overlay canvas. However, not - /// every platform view which is prerolled ends up being composited (it may be - /// clipped out and not actually drawn). This means that we may end up - /// overallocating canvases. This isn't a problem in practice, however, as - /// unused recording canvases are simply deleted at the end of the frame. - Iterable getOverlayCanvases() { - return _context.pictureRecordersCreatedDuringPreroll + /// Returns a list of recording canvases which the pictures in the upcoming + /// paint step will be drawn into. These recording canvases are combined into + /// an N-way canvas for the rasterizer to record clip and transform operations + /// during the measure step. + Iterable getPictureCanvases() { + return _context.measuringPictureRecorders.values + .map((CkPictureRecorder r) => r.recordingCanvas!); + } + + /// Returns a list of canvases for the optimized rendering. These are used in + /// the paint step. + Iterable getOptimizedCanvases() { + return _context.optimizedCanvasRecorders! .map((CkPictureRecorder r) => r.recordingCanvas!); } void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) { - final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); - _context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder); - // Do nothing if the params didn't change. if (_currentCompositionParams[viewId] == params) { // If the view was prerolled but not composited, then it needs to be @@ -109,30 +109,38 @@ class HtmlViewEmbedder { _viewsToRecomposite.add(viewId); } + /// Record that a picture recorder is needed for [picture] to be measured. + void prerollPicture(PictureLayer picture) { + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); + _context.measuringPictureRecorders[picture] = pictureRecorder; + } + + /// Returns the canvas that was created to measure [picture]. + CkCanvas getMeasuringCanvasFor(PictureLayer picture) { + return _context.measuringPictureRecorders[picture]!.recordingCanvas!; + } + + /// Adds the picture recorder associated with [picture] to the unoptimized + /// scene. + void addPictureToUnoptimizedScene(PictureLayer picture) { + final CkPictureRecorder recorder = + _context.measuringPictureRecorders[picture]!; + _context.sceneElements.add(PictureSceneElement(picture, recorder)); + } + /// Prepares to composite [viewId]. - /// - /// If this returns a [CkCanvas], then that canvas should be the new leaf - /// node. Otherwise, keep the same leaf node. - CkCanvas? compositeEmbeddedView(int viewId) { + void compositeEmbeddedView(int viewId) { // Ensure platform view with `viewId` is injected into the `rasterizer.view`. rasterizer.view.dom.injectPlatformView(viewId); - final int overlayIndex = _context.viewCount; _compositionOrder.add(viewId); - _context.viewCount++; - - CkPictureRecorder? recorderToUseForRendering; - if (overlayIndex < _context.pictureRecordersCreatedDuringPreroll.length) { - recorderToUseForRendering = - _context.pictureRecordersCreatedDuringPreroll[overlayIndex]; - _context.pictureRecorders.add(recorderToUseForRendering); - } + _context.sceneElements.add(PlatformViewSceneElement(viewId)); if (_viewsToRecomposite.contains(viewId)) { _compositeWithParams(viewId, _currentCompositionParams[viewId]!); _viewsToRecomposite.remove(viewId); } - return recorderToUseForRendering?.recordingCanvas; } void _compositeWithParams(int platformViewId, EmbeddedViewParams params) { @@ -355,14 +363,54 @@ class HtmlViewEmbedder { sceneHost.append(_svgPathDefs!); } - Future submitFrame(CkPicture basePicture) async { - final List pictures = [basePicture]; - for (final CkPictureRecorder recorder in _context.pictureRecorders) { - pictures.add(recorder.endRecording()); - } + /// Optimizes the scene to use the fewest possible canvases. This sets up + /// the final paint pass to paint the pictures into the optimized canvases. + void optimizeRendering() { + final Map scenePictureToRawPicture = + {}; + final Iterable unoptimizedRendering = + _context.sceneElements.map((SceneElement element) { + if (element is PictureSceneElement) { + final CkPicture scenePicture = element.pictureRecorder.endRecording(); + element.scenePicture = scenePicture; + scenePictureToRawPicture[scenePicture] = element.picture; + return element; + } else { + return element; + } + }); Rendering rendering = createOptimizedRendering( - pictures, _compositionOrder, _currentCompositionParams); + unoptimizedRendering, _currentCompositionParams); rendering = _modifyRenderingForMaxCanvases(rendering); + _context.optimizedRendering = rendering; + // Create new picture recorders for the optimized render canvases and record + // which pictures go in which canvas. + final List optimizedCanvasRecorders = + []; + final Map pictureToOptimizedCanvasMap = + {}; + for (final RenderingRenderCanvas renderCanvas in rendering.canvases) { + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); + optimizedCanvasRecorders.add(pictureRecorder); + for (final CkPicture picture in renderCanvas.pictures) { + pictureToOptimizedCanvasMap[scenePictureToRawPicture[picture]!] = + pictureRecorder; + } + } + _context.optimizedCanvasRecorders = optimizedCanvasRecorders; + _context.pictureToOptimizedCanvasMap = pictureToOptimizedCanvasMap; + } + + /// Returns the canvas that this picture layer should draw into in the + /// optimized scene. + CkCanvas getOptimizedCanvasFor(PictureLayer picture) { + assert(_context.optimizedRendering != null); + return _context.pictureToOptimizedCanvasMap![picture]!.recordingCanvas!; + } + + Future submitFrame() async { + final Rendering rendering = _context.optimizedRendering!; _updateDomForNewRendering(rendering); if (rendering.equalsForRendering(_activeRendering)) { // Copy the display canvases to the new rendering. @@ -375,13 +423,17 @@ class HtmlViewEmbedder { _activeRendering = rendering; final List renderCanvases = rendering.canvases; + int renderCanvasIndex = 0; for (final RenderingRenderCanvas renderCanvas in renderCanvases) { + final CkPicture renderPicture = _context + .optimizedCanvasRecorders![renderCanvasIndex++] + .endRecording(); await rasterizer.rasterizeToCanvas( - renderCanvas.displayCanvas!, renderCanvas.pictures); + renderCanvas.displayCanvas!, [renderPicture]); } for (final CkPictureRecorder recorder - in _context.pictureRecordersCreatedDuringPreroll) { + in _context.measuringPictureRecorders.values) { if (recorder.isRecording) { recorder.endRecording(); } @@ -393,11 +445,11 @@ class HtmlViewEmbedder { debugBoundsCanvas ??= rasterizer.displayFactory.getCanvas(); final CkPictureRecorder boundsRecorder = CkPictureRecorder(); final CkCanvas boundsCanvas = boundsRecorder.beginRecording( - ui.Rect.fromLTWH( - 0, - 0, - _frameSize.width.toDouble(), - _frameSize.height.toDouble(), + ui.Rect.fromLTWH( + 0, + 0, + _frameSize.width.toDouble(), + _frameSize.height.toDouble(), ), ); final CkPaint platformViewBoundsPaint = CkPaint() @@ -903,20 +955,45 @@ class MutatorsStack extends Iterable { Iterable get reversed => _mutators; } +sealed class SceneElement {} + +class PictureSceneElement extends SceneElement { + PictureSceneElement(this.picture, this.pictureRecorder); + + final PictureLayer picture; + final CkPictureRecorder pictureRecorder; + + /// The picture as it would be painted in the final scene, with clips and + /// transforms applied. This is set by [optimizeRendering]. + CkPicture? scenePicture; +} + +class PlatformViewSceneElement extends SceneElement { + PlatformViewSceneElement(this.viewId); + + final int viewId; +} + /// The state for the current frame. class EmbedderFrameContext { - /// Picture recorders which were created during the preroll phase. - /// - /// These picture recorders will be "claimed" in the paint phase by platform - /// views being composited into the scene. - final List pictureRecordersCreatedDuringPreroll = - []; + /// Picture recorders which were created d the final bounds of the picture in the scene. + final Map measuringPictureRecorders = + {}; - /// Picture recorders which were actually used in the paint phase. - /// - /// This is a subset of [_pictureRecordersCreatedDuringPreroll]. - final List pictureRecorders = []; + /// List of picture recorders and platform view ids in the order they were + /// painted. + final List sceneElements = []; - /// The number of platform views in this frame. - int viewCount = 0; + /// The optimized rendering for this frame. This is set by calling + /// [optimizeRendering]. + Rendering? optimizedRendering; + + /// The picture recorders for the optimized rendering. This is set by calling + /// [optimizeRendering]. + List? optimizedCanvasRecorders; + + /// A map from the original PictureLayer to the picture recorder it should go + /// into in the optimized rendering. This is set by calling + /// [optimizedRendering]. + Map? pictureToOptimizedCanvasMap; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart index 010fc01278..2ccbec7b7b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart @@ -4,18 +4,10 @@ import 'package:ui/ui.dart' as ui; -import '../color_filter.dart'; import '../vector_math.dart'; -import 'canvas.dart'; -import 'canvaskit_api.dart'; -import 'color_filter.dart'; -import 'embedded_views.dart'; -import 'image_filter.dart'; -import 'n_way_canvas.dart'; -import 'painting.dart'; +import 'layer_visitor.dart'; import 'path.dart'; import 'picture.dart'; -import 'raster_cache.dart'; /// A layer to be composed into a scene. /// @@ -31,15 +23,8 @@ abstract class Layer implements ui.EngineLayer { /// Whether or not this layer actually needs to be painted in the scene. bool get needsPainting => !paintBounds.isEmpty; - /// Pre-process this layer before painting. - /// - /// In this step, we compute the estimated [paintBounds] as well as - /// apply heuristics to prepare the render cache for pictures that - /// should be cached. - void preroll(PrerollContext prerollContext, Matrix4 matrix); - - /// Paint this layer into the scene. - void paint(PaintContext paintContext); + /// Implement layer visitor. + void accept(LayerVisitor visitor, T childData); // TODO(dnfield): Implement ui.EngineLayer.dispose for CanvasKit. // https://github.com/flutter/flutter/issues/82878 @@ -47,109 +32,19 @@ abstract class Layer implements ui.EngineLayer { void dispose() {} } -/// A context shared by all layers during the preroll pass. -class PrerollContext { - PrerollContext(this.rasterCache, this.viewEmbedder); - - /// A raster cache. Used to register candidates for caching. - final RasterCache? rasterCache; - - /// A compositor for embedded HTML views. - final HtmlViewEmbedder? viewEmbedder; - - final MutatorsStack mutatorsStack = MutatorsStack(); - - ui.Rect get cullRect { - ui.Rect cullRect = ui.Rect.largest; - for (final Mutator m in mutatorsStack) { - ui.Rect clipRect; - switch (m.type) { - case MutatorType.clipRect: - clipRect = m.rect!; - case MutatorType.clipRRect: - clipRect = m.rrect!.outerRect; - case MutatorType.clipPath: - clipRect = m.path!.getBounds(); - default: - continue; - } - cullRect = cullRect.intersect(clipRect); - } - return cullRect; - } -} - -/// A context shared by all layers during the paint pass. -class PaintContext { - PaintContext( - this.internalNodesCanvas, - this.leafNodesCanvas, - this.rasterCache, - this.viewEmbedder, - ); - - /// A multi-canvas that applies clips, transforms, and opacity - /// operations to all canvases (root canvas and overlay canvases for the - /// platform views). - CkNWayCanvas internalNodesCanvas; - - /// The canvas for leaf nodes to paint to. - CkCanvas? leafNodesCanvas; - - /// A raster cache potentially containing pre-rendered pictures. - final RasterCache? rasterCache; - - /// A compositor for embedded HTML views. - final HtmlViewEmbedder? viewEmbedder; -} - /// A layer that contains child layers. abstract class ContainerLayer extends Layer { - final List _layers = []; + final List children = []; /// The list of child layers. /// /// Useful in tests. - List get debugLayers => _layers; + List get debugLayers => children; /// Register [child] as a child of this layer. void add(Layer child) { child.parent = this; - _layers.add(child); - } - - @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - paintBounds = prerollChildren(prerollContext, matrix); - } - - /// Run [preroll] on all of the child layers. - /// - /// Returns a [Rect] that covers the paint bounds of all of the child layers. - /// If all of the child layers have empty paint bounds, then the returned - /// [Rect] is empty. - ui.Rect prerollChildren(PrerollContext context, Matrix4 childMatrix) { - ui.Rect childPaintBounds = ui.Rect.zero; - for (final Layer layer in _layers) { - layer.preroll(context, childMatrix); - if (childPaintBounds.isEmpty) { - childPaintBounds = layer.paintBounds; - } else if (!layer.paintBounds.isEmpty) { - childPaintBounds = childPaintBounds.expandToInclude(layer.paintBounds); - } - } - return childPaintBounds; - } - - /// Calls [paint] on all child layers that need painting. - void paintChildren(PaintContext context) { - assert(needsPainting); - - for (final Layer layer in _layers) { - if (layer.needsPainting) { - layer.paint(context); - } - } + children.add(child); } } @@ -159,36 +54,21 @@ abstract class ContainerLayer extends Layer { /// to [LayerSceneBuilder] without requiring a [ContainerLayer]. class RootLayer extends ContainerLayer { @override - void paint(PaintContext paintContext) { - paintChildren(paintContext); + void accept(LayerVisitor visitor, T childData) { + visitor.visitRoot(this, childData); } } class BackdropFilterEngineLayer extends ContainerLayer implements ui.BackdropFilterEngineLayer { - BackdropFilterEngineLayer(this._filter, this._blendMode); + BackdropFilterEngineLayer(this.filter, this.blendMode); - final ui.ImageFilter _filter; - final ui.BlendMode _blendMode; + final ui.ImageFilter filter; + final ui.BlendMode blendMode; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final ui.Rect childBounds = prerollChildren(prerollContext, matrix); - paintBounds = childBounds.expandToInclude(prerollContext.cullRect); - } - - @override - void paint(PaintContext paintContext) { - final CkPaint paint = CkPaint()..blendMode = _blendMode; - - // Only apply the backdrop filter to the current canvas. If we apply the - // backdrop filter to every canvas (i.e. by applying it to the - // [internalNodesCanvas]), then later when we compose the canvases into a - // single canvas, the backdrop filter will be applied multiple times. - final CkCanvas currentCanvas = paintContext.leafNodesCanvas!; - currentCanvas.saveLayerWithFilter(paintBounds, _filter, paint); - paintChildren(paintContext); - currentCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitBackdropFilter(this, childData); } // TODO(dnfield): dispose of the _filter @@ -198,189 +78,76 @@ class BackdropFilterEngineLayer extends ContainerLayer /// A layer that clips its child layers by a given [Path]. class ClipPathEngineLayer extends ContainerLayer implements ui.ClipPathEngineLayer { - ClipPathEngineLayer(this._clipPath, this._clipBehavior) - : assert(_clipBehavior != ui.Clip.none); + ClipPathEngineLayer(this.clipPath, this.clipBehavior) + : assert(clipBehavior != ui.Clip.none); /// The path used to clip child layers. - final CkPath _clipPath; - final ui.Clip _clipBehavior; + final CkPath clipPath; + final ui.Clip clipBehavior; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - prerollContext.mutatorsStack.pushClipPath(_clipPath); - final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); - final ui.Rect clipBounds = _clipPath.getBounds(); - if (childPaintBounds.overlaps(clipBounds)) { - paintBounds = childPaintBounds.intersect(clipBounds); - } - prerollContext.mutatorsStack.pop(); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas - .clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge); - - if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.internalNodesCanvas.saveLayer(paintBounds, null); - } - paintChildren(paintContext); - if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.internalNodesCanvas.restore(); - } - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitClipPath(this, childData); } } /// A layer that clips its child layers by a given [Rect]. class ClipRectEngineLayer extends ContainerLayer implements ui.ClipRectEngineLayer { - ClipRectEngineLayer(this._clipRect, this._clipBehavior) - : assert(_clipBehavior != ui.Clip.none); + ClipRectEngineLayer(this.clipRect, this.clipBehavior) + : assert(clipBehavior != ui.Clip.none); /// The rectangle used to clip child layers. - final ui.Rect _clipRect; - final ui.Clip _clipBehavior; + final ui.Rect clipRect; + final ui.Clip clipBehavior; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - prerollContext.mutatorsStack.pushClipRect(_clipRect); - final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); - if (childPaintBounds.overlaps(_clipRect)) { - paintBounds = childPaintBounds.intersect(_clipRect); - } - prerollContext.mutatorsStack.pop(); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas.clipRect( - _clipRect, - ui.ClipOp.intersect, - _clipBehavior != ui.Clip.hardEdge, - ); - if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.internalNodesCanvas.saveLayer(_clipRect, null); - } - paintChildren(paintContext); - if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.internalNodesCanvas.restore(); - } - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitClipRect(this, childData); } } /// A layer that clips its child layers by a given [RRect]. class ClipRRectEngineLayer extends ContainerLayer implements ui.ClipRRectEngineLayer { - ClipRRectEngineLayer(this._clipRRect, this._clipBehavior) - : assert(_clipBehavior != ui.Clip.none); + ClipRRectEngineLayer(this.clipRRect, this.clipBehavior) + : assert(clipBehavior != ui.Clip.none); /// The rounded rectangle used to clip child layers. - final ui.RRect _clipRRect; - final ui.Clip? _clipBehavior; + final ui.RRect clipRRect; + final ui.Clip? clipBehavior; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - prerollContext.mutatorsStack.pushClipRRect(_clipRRect); - final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); - if (childPaintBounds.overlaps(_clipRRect.outerRect)) { - paintBounds = childPaintBounds.intersect(_clipRRect.outerRect); - } - prerollContext.mutatorsStack.pop(); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas - .clipRRect(_clipRRect, _clipBehavior != ui.Clip.hardEdge); - if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.internalNodesCanvas.saveLayer(paintBounds, null); - } - paintChildren(paintContext); - if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.internalNodesCanvas.restore(); - } - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitClipRRect(this, childData); } } /// A layer that paints its children with the given opacity. class OpacityEngineLayer extends ContainerLayer implements ui.OpacityEngineLayer { - OpacityEngineLayer(this._alpha, this._offset); + OpacityEngineLayer(this.alpha, this.offset); - final int _alpha; - final ui.Offset _offset; + final int alpha; + final ui.Offset offset; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final Matrix4 childMatrix = Matrix4.copy(matrix); - childMatrix.translate(_offset.dx, _offset.dy); - prerollContext.mutatorsStack - .pushTransform(Matrix4.translationValues(_offset.dx, _offset.dy, 0.0)); - prerollContext.mutatorsStack.pushOpacity(_alpha); - super.preroll(prerollContext, childMatrix); - prerollContext.mutatorsStack.pop(); - prerollContext.mutatorsStack.pop(); - paintBounds = paintBounds.translate(_offset.dx, _offset.dy); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - final CkPaint paint = CkPaint(); - paint.color = ui.Color.fromARGB(_alpha, 0, 0, 0); - - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas.translate(_offset.dx, _offset.dy); - - final ui.Rect saveLayerBounds = paintBounds.shift(-_offset); - - paintContext.internalNodesCanvas.saveLayer(saveLayerBounds, paint); - paintChildren(paintContext); - // Restore twice: once for the translate and once for the saveLayer. - paintContext.internalNodesCanvas.restore(); - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitOpacity(this, childData); } } /// A layer that transforms its child layers by the given transform matrix. class TransformEngineLayer extends ContainerLayer implements ui.TransformEngineLayer { - TransformEngineLayer(this._transform); + TransformEngineLayer(this.transform); /// The matrix with which to transform the child layers. - final Matrix4 _transform; + final Matrix4 transform; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final Matrix4 childMatrix = matrix.multiplied(_transform); - prerollContext.mutatorsStack.pushTransform(_transform); - final ui.Rect childPaintBounds = - prerollChildren(prerollContext, childMatrix); - paintBounds = _transform.transformRect(childPaintBounds); - prerollContext.mutatorsStack.pop(); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas.transform(_transform.storage); - paintChildren(paintContext); - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitTransform(this, childData); } } @@ -393,59 +160,24 @@ class OffsetEngineLayer extends TransformEngineLayer implements ui.OffsetEngineLayer { OffsetEngineLayer(double dx, double dy) : super(Matrix4.translationValues(dx, dy, 0.0)); + + @override + void accept(LayerVisitor visitor, T childData) { + visitor.visitOffset(this, childData); + } } /// A layer that applies an [ui.ImageFilter] to its children. class ImageFilterEngineLayer extends ContainerLayer implements ui.ImageFilterEngineLayer { - ImageFilterEngineLayer(this._filter, this._offset); + ImageFilterEngineLayer(this.filter, this.offset); - final ui.Offset _offset; - final ui.ImageFilter _filter; + final ui.Offset offset; + final ui.ImageFilter filter; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final Matrix4 childMatrix = Matrix4.copy(matrix); - childMatrix.translate(_offset.dx, _offset.dy); - prerollContext.mutatorsStack - .pushTransform(Matrix4.translationValues(_offset.dx, _offset.dy, 0.0)); - final CkManagedSkImageFilterConvertible convertible; - if (_filter is ui.ColorFilter) { - convertible = createCkColorFilter(_filter as EngineColorFilter)!; - } else { - convertible = _filter as CkManagedSkImageFilterConvertible; - } - ui.Rect childPaintBounds = - prerollChildren(prerollContext, childMatrix); - childPaintBounds = childPaintBounds.translate(_offset.dx, _offset.dy); - if (_filter is ui.ColorFilter) { - // If the filter is a ColorFilter, the extended paint bounds will be the - // entire screen, which is not what we want. - paintBounds = childPaintBounds; - } else { - convertible.withSkImageFilter((skFilter) { - paintBounds = rectFromSkIRect( - skFilter.getOutputBounds(toSkRect(childPaintBounds)), - ); - }); - } - prerollContext.mutatorsStack.pop(); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - final ui.Rect offsetPaintBounds = paintBounds.shift(-_offset); - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas.translate(_offset.dx, _offset.dy); - paintContext.internalNodesCanvas - .clipRect(offsetPaintBounds, ui.ClipOp.intersect, false); - final CkPaint paint = CkPaint(); - paint.imageFilter = _filter; - paintContext.internalNodesCanvas.saveLayer(offsetPaintBounds, paint); - paintChildren(paintContext); - paintContext.internalNodesCanvas.restore(); - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitImageFilter(this, childData); } // TODO(dnfield): dispose of the _filter @@ -463,25 +195,8 @@ class ShaderMaskEngineLayer extends ContainerLayer final ui.FilterQuality filterQuality; @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.internalNodesCanvas.saveLayer(paintBounds, null); - paintChildren(paintContext); - - final CkPaint paint = CkPaint(); - paint.shader = shader; - paint.blendMode = blendMode; - paint.filterQuality = filterQuality; - - paintContext.leafNodesCanvas!.save(); - paintContext.leafNodesCanvas!.translate(maskRect.left, maskRect.top); - - paintContext.leafNodesCanvas!.drawRect( - ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), paint); - paintContext.leafNodesCanvas!.restore(); - - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitShaderMask(this, childData); } } @@ -502,19 +217,8 @@ class PictureLayer extends Layer { final bool willChange; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - paintBounds = picture.cullRect.shift(offset); - } - - @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.leafNodesCanvas!.save(); - paintContext.leafNodesCanvas!.translate(offset.dx, offset.dy); - - paintContext.leafNodesCanvas!.drawPicture(picture); - paintContext.leafNodesCanvas!.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitPicture(this, childData); } } @@ -526,26 +230,8 @@ class ColorFilterEngineLayer extends ContainerLayer final ui.ColorFilter filter; @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - final CkPaint paint = CkPaint(); - paint.colorFilter = filter; - - // We need to clip because if the ColorFilter affects transparent black, - // then it will fill the entire `cullRect` of the picture, ignoring the - // `paintBounds` passed to `saveLayer`. See: - // https://github.com/flutter/flutter/issues/88866 - paintContext.internalNodesCanvas.save(); - - // TODO(hterkelsen): Only clip if the ColorFilter affects transparent black. - paintContext.internalNodesCanvas - .clipRect(paintBounds, ui.ClipOp.intersect, false); - - paintContext.internalNodesCanvas.saveLayer(paintBounds, paint); - paintChildren(paintContext); - paintContext.internalNodesCanvas.restore(); - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor, T childData) { + visitor.visitColorFilter(this, childData); } } @@ -559,27 +245,7 @@ class PlatformViewLayer extends Layer { final double height; @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - paintBounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); - - /// ViewEmbedder is set to null when screenshotting. Therefore, skip - /// rendering - prerollContext.viewEmbedder?.prerollCompositeEmbeddedView( - viewId, - EmbeddedViewParams( - offset, - ui.Size(width, height), - prerollContext.mutatorsStack, - ), - ); - } - - @override - void paint(PaintContext paintContext) { - final CkCanvas? canvas = - paintContext.viewEmbedder?.compositeEmbeddedView(viewId); - if (canvas != null) { - paintContext.leafNodesCanvas = canvas; - } + void accept(LayerVisitor visitor, T childData) { + visitor.visitPlatformView(this, childData); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart index 2aca00f35d..faac9beb83 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart @@ -10,6 +10,7 @@ import '../vector_math.dart'; import 'canvas.dart'; import 'embedded_views.dart'; import 'layer.dart'; +import 'layer_visitor.dart'; import 'n_way_canvas.dart'; import 'picture_recorder.dart'; import 'raster_cache.dart'; @@ -31,11 +32,25 @@ class LayerTree { /// to raster. If [ignoreRasterCache] is `true`, then there will be no /// attempt to register pictures to cache. void preroll(Frame frame, {bool ignoreRasterCache = false}) { - final PrerollContext context = PrerollContext( - ignoreRasterCache ? null : frame.rasterCache, - frame.viewEmbedder, + final PrerollVisitor prerollVisitor = PrerollVisitor(frame.viewEmbedder); + rootLayer.accept(prerollVisitor, Matrix4.identity()); + } + + /// Performs a paint pass with a recording canvas for each picture in the + /// tree. This paint pass is just used to measure the bounds for each picture + /// so we can optimize the total number of canvases required. + void measure(Frame frame, {bool ignoreRasterCache = false}) { + final CkNWayCanvas nWayCanvas = CkNWayCanvas(); + final Iterable recordingCanvases = + frame.viewEmbedder!.getPictureCanvases(); + recordingCanvases.forEach(nWayCanvas.addCanvas); + final MeasureVisitor measureVisitor = MeasureVisitor( + nWayCanvas, + frame.viewEmbedder!, ); - rootLayer.preroll(context, Matrix4.identity()); + if (rootLayer.needsPainting) { + rootLayer.accept(measureVisitor, null); + } } /// Paints the layer tree into the given [frame]. @@ -44,18 +59,15 @@ class LayerTree { /// not be used. void paint(Frame frame, {bool ignoreRasterCache = false}) { final CkNWayCanvas internalNodesCanvas = CkNWayCanvas(); - internalNodesCanvas.addCanvas(frame.canvas); final Iterable overlayCanvases = - frame.viewEmbedder!.getOverlayCanvases(); + frame.viewEmbedder!.getOptimizedCanvases(); overlayCanvases.forEach(internalNodesCanvas.addCanvas); - final PaintContext context = PaintContext( + final PaintVisitor paintVisitor = PaintVisitor( internalNodesCanvas, - frame.canvas, - ignoreRasterCache ? null : frame.rasterCache, - frame.viewEmbedder, + frame.viewEmbedder!, ); if (rootLayer.needsPainting) { - rootLayer.paint(context); + rootLayer.accept(paintVisitor, null); } } @@ -65,15 +77,15 @@ class LayerTree { ui.Picture flatten(ui.Size size) { final CkPictureRecorder recorder = CkPictureRecorder(); final CkCanvas canvas = recorder.beginRecording(ui.Offset.zero & size); - final PrerollContext prerollContext = PrerollContext(null, null); - rootLayer.preroll(prerollContext, Matrix4.identity()); + final PrerollVisitor prerollVisitor = PrerollVisitor(null); + rootLayer.accept(prerollVisitor, Matrix4.identity()); final CkNWayCanvas internalNodesCanvas = CkNWayCanvas(); internalNodesCanvas.addCanvas(canvas); - final PaintContext paintContext = - PaintContext(internalNodesCanvas, canvas, null, null); + final PaintVisitor paintVisitor = + PaintVisitor.forToImage(internalNodesCanvas, canvas); if (rootLayer.needsPainting) { - rootLayer.paint(paintContext); + rootLayer.accept(paintVisitor, null); } return recorder.endRecording(); } @@ -81,10 +93,7 @@ class LayerTree { /// A single frame to be rendered. class Frame { - Frame(this.canvas, this.rasterCache, this.viewEmbedder); - - /// The canvas to render this frame to. - final CkCanvas canvas; + Frame(this.rasterCache, this.viewEmbedder); /// A cache of pre-rastered pictures. final RasterCache? rasterCache; @@ -96,6 +105,8 @@ class Frame { bool raster(LayerTree layerTree, {bool ignoreRasterCache = false}) { timeAction(kProfilePrerollFrame, () { layerTree.preroll(this, ignoreRasterCache: ignoreRasterCache); + layerTree.measure(this, ignoreRasterCache: ignoreRasterCache); + viewEmbedder?.optimizeRendering(); }); timeAction(kProfileApplyFrame, () { layerTree.paint(this, ignoreRasterCache: ignoreRasterCache); @@ -110,7 +121,7 @@ class CompositorContext { RasterCache? rasterCache; /// Acquire a frame using this compositor's settings. - Frame acquireFrame(CkCanvas canvas, HtmlViewEmbedder? viewEmbedder) { - return Frame(canvas, rasterCache, viewEmbedder); + Frame acquireFrame(HtmlViewEmbedder? viewEmbedder) { + return Frame(rasterCache, viewEmbedder); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart new file mode 100644 index 0000000000..78b2d1fcd5 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart @@ -0,0 +1,680 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:ui/ui.dart' as ui; + +import '../color_filter.dart'; +import '../vector_math.dart'; +import 'canvas.dart'; +import 'canvaskit_api.dart'; +import 'color_filter.dart'; +import 'embedded_views.dart'; +import 'image_filter.dart'; +import 'layer.dart'; +import 'n_way_canvas.dart'; +import 'painting.dart'; + +abstract class LayerVisitor { + void visitRoot(RootLayer root, T childData); + void visitBackdropFilter( + BackdropFilterEngineLayer backdropFilter, T childData); + void visitClipPath(ClipPathEngineLayer clipPath, T childData); + void visitClipRect(ClipRectEngineLayer clipRect, T childData); + void visitClipRRect(ClipRRectEngineLayer clipRRect, T childData); + void visitOpacity(OpacityEngineLayer opacity, T childData); + void visitTransform(TransformEngineLayer transform, T childData); + void visitOffset(OffsetEngineLayer offset, T childData); + void visitImageFilter(ImageFilterEngineLayer imageFilter, T childData); + void visitShaderMask(ShaderMaskEngineLayer shaderMask, T childData); + void visitPicture(PictureLayer picture, T childData); + void visitColorFilter(ColorFilterEngineLayer colorFilter, T childData); + void visitPlatformView(PlatformViewLayer platformView, T childData); +} + +/// Pre-process the layer tree before painting. +/// +/// In this step, we compute the estimated [paintBounds] as well as +/// apply heuristics to prepare the render cache for pictures that +/// should be cached. +class PrerollVisitor extends LayerVisitor { + PrerollVisitor(this.viewEmbedder); + + final MutatorsStack mutatorsStack = MutatorsStack(); + + /// A compositor for embedded HTML views. + final HtmlViewEmbedder? viewEmbedder; + + ui.Rect get cullRect { + ui.Rect cullRect = ui.Rect.largest; + for (final Mutator m in mutatorsStack) { + ui.Rect clipRect; + switch (m.type) { + case MutatorType.clipRect: + clipRect = m.rect!; + case MutatorType.clipRRect: + clipRect = m.rrect!.outerRect; + case MutatorType.clipPath: + clipRect = m.path!.getBounds(); + default: + continue; + } + cullRect = cullRect.intersect(clipRect); + } + return cullRect; + } + + /// Run [preroll] on all of the child layers. + /// + /// Returns a [Rect] that covers the paint bounds of all of the child layers. + /// If all of the child layers have empty paint bounds, then the returned + /// [Rect] is empty. + ui.Rect prerollChildren(ContainerLayer layer, Matrix4 childMatrix) { + ui.Rect childPaintBounds = ui.Rect.zero; + for (final Layer layer in layer.children) { + layer.accept(this, childMatrix); + if (childPaintBounds.isEmpty) { + childPaintBounds = layer.paintBounds; + } else if (!layer.paintBounds.isEmpty) { + childPaintBounds = childPaintBounds.expandToInclude(layer.paintBounds); + } + } + return childPaintBounds; + } + + void prerollContainerLayer(ContainerLayer container, Matrix4 matrix) { + container.paintBounds = prerollChildren(container, matrix); + } + + @override + void visitRoot(RootLayer root, Matrix4 childData) { + prerollContainerLayer(root, childData); + } + + @override + void visitBackdropFilter( + BackdropFilterEngineLayer backdropFilter, Matrix4 childData) { + final ui.Rect childBounds = prerollChildren(backdropFilter, childData); + backdropFilter.paintBounds = childBounds.expandToInclude(cullRect); + } + + @override + void visitClipPath(ClipPathEngineLayer clipPath, Matrix4 childData) { + mutatorsStack.pushClipPath(clipPath.clipPath); + final ui.Rect childPaintBounds = prerollChildren(clipPath, childData); + final ui.Rect clipBounds = clipPath.clipPath.getBounds(); + if (childPaintBounds.overlaps(clipBounds)) { + clipPath.paintBounds = childPaintBounds.intersect(clipBounds); + } + mutatorsStack.pop(); + } + + @override + void visitClipRRect(ClipRRectEngineLayer clipRRect, Matrix4 childData) { + mutatorsStack.pushClipRRect(clipRRect.clipRRect); + final ui.Rect childPaintBounds = prerollChildren(clipRRect, childData); + if (childPaintBounds.overlaps(clipRRect.clipRRect.outerRect)) { + clipRRect.paintBounds = + childPaintBounds.intersect(clipRRect.clipRRect.outerRect); + } + mutatorsStack.pop(); + } + + @override + void visitClipRect(ClipRectEngineLayer clipRect, Matrix4 childData) { + mutatorsStack.pushClipRect(clipRect.clipRect); + final ui.Rect childPaintBounds = prerollChildren(clipRect, childData); + if (childPaintBounds.overlaps(clipRect.clipRect)) { + clipRect.paintBounds = childPaintBounds.intersect(clipRect.clipRect); + } + mutatorsStack.pop(); + } + + @override + void visitColorFilter(ColorFilterEngineLayer colorFilter, Matrix4 childData) { + prerollContainerLayer(colorFilter, childData); + } + + @override + void visitImageFilter(ImageFilterEngineLayer imageFilter, Matrix4 childData) { + final Matrix4 childMatrix = Matrix4.copy(childData); + childMatrix.translate(imageFilter.offset.dx, imageFilter.offset.dy); + mutatorsStack.pushTransform(Matrix4.translationValues( + imageFilter.offset.dx, imageFilter.offset.dy, 0.0)); + final CkManagedSkImageFilterConvertible convertible; + if (imageFilter.filter is ui.ColorFilter) { + convertible = + createCkColorFilter(imageFilter.filter as EngineColorFilter)!; + } else { + convertible = imageFilter.filter as CkManagedSkImageFilterConvertible; + } + ui.Rect childPaintBounds = prerollChildren(imageFilter, childMatrix); + childPaintBounds = childPaintBounds.translate( + imageFilter.offset.dx, imageFilter.offset.dy); + if (imageFilter.filter is ui.ColorFilter) { + // If the filter is a ColorFilter, the extended paint bounds will be the + // entire screen, which is not what we want. + imageFilter.paintBounds = childPaintBounds; + } else { + convertible.withSkImageFilter((SkImageFilter skFilter) { + imageFilter.paintBounds = rectFromSkIRect( + skFilter.getOutputBounds(toSkRect(childPaintBounds)), + ); + }); + } + mutatorsStack.pop(); + } + + @override + void visitOffset(OffsetEngineLayer offset, Matrix4 childData) { + visitTransform(offset, childData); + } + + @override + void visitOpacity(OpacityEngineLayer opacity, Matrix4 childData) { + final Matrix4 childMatrix = Matrix4.copy(childData); + childMatrix.translate(opacity.offset.dx, opacity.offset.dy); + mutatorsStack.pushTransform( + Matrix4.translationValues(opacity.offset.dx, opacity.offset.dy, 0.0)); + mutatorsStack.pushOpacity(opacity.alpha); + prerollContainerLayer(opacity, childMatrix); + mutatorsStack.pop(); + mutatorsStack.pop(); + opacity.paintBounds = + opacity.paintBounds.translate(opacity.offset.dx, opacity.offset.dy); + } + + @override + void visitPicture(PictureLayer picture, Matrix4 childData) { + picture.paintBounds = picture.picture.cullRect.shift(picture.offset); + viewEmbedder?.prerollPicture(picture); + } + + @override + void visitPlatformView(PlatformViewLayer platformView, Matrix4 childData) { + platformView.paintBounds = ui.Rect.fromLTWH( + platformView.offset.dx, + platformView.offset.dy, + platformView.width, + platformView.height, + ); + + /// ViewEmbedder is set to null when screenshotting. Therefore, skip + /// rendering + viewEmbedder?.prerollCompositeEmbeddedView( + platformView.viewId, + EmbeddedViewParams( + platformView.offset, + ui.Size(platformView.width, platformView.height), + mutatorsStack, + ), + ); + } + + @override + void visitShaderMask(ShaderMaskEngineLayer shaderMask, Matrix4 childData) { + shaderMask.paintBounds = prerollChildren(shaderMask, childData); + } + + @override + void visitTransform(TransformEngineLayer transform, Matrix4 childData) { + final Matrix4 childMatrix = childData.multiplied(transform.transform); + mutatorsStack.pushTransform(transform.transform); + final ui.Rect childPaintBounds = prerollChildren(transform, childMatrix); + transform.paintBounds = transform.transform.transformRect(childPaintBounds); + mutatorsStack.pop(); + } +} + +/// A layer visitor which measures the pictures that make up the scene and +/// prepares for them to be optimized into few canvases. +class MeasureVisitor extends LayerVisitor { + MeasureVisitor( + this.nWayCanvas, + this.viewEmbedder, + ); + + /// A multi-canvas that applies clips, transforms, and opacity + /// operations to all canvases (root canvas and overlay canvases for the + /// platform views). + CkNWayCanvas nWayCanvas; + + /// A compositor for embedded HTML views. + final HtmlViewEmbedder viewEmbedder; + + /// Measures all child layers that need painting. + void measureChildren(ContainerLayer container) { + assert(container.needsPainting); + + for (final Layer layer in container.children) { + if (layer.needsPainting) { + layer.accept(this, null); + } + } + } + + @override + void visitRoot(RootLayer root, void childData) { + measureChildren(root); + } + + @override + void visitBackdropFilter( + BackdropFilterEngineLayer backdropFilter, void childData) { + measureChildren(backdropFilter); + } + + @override + void visitClipPath(ClipPathEngineLayer clipPath, void childData) { + assert(clipPath.needsPainting); + + nWayCanvas.save(); + nWayCanvas.clipPath( + clipPath.clipPath, clipPath.clipBehavior != ui.Clip.hardEdge); + + if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipPath.paintBounds, null); + } + measureChildren(clipPath); + if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + + @override + void visitClipRect(ClipRectEngineLayer clipRect, void childData) { + assert(clipRect.needsPainting); + + nWayCanvas.save(); + nWayCanvas.clipRect( + clipRect.clipRect, + ui.ClipOp.intersect, + clipRect.clipBehavior != ui.Clip.hardEdge, + ); + if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipRect.clipRect, null); + } + measureChildren(clipRect); + if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + + @override + void visitClipRRect(ClipRRectEngineLayer clipRRect, void childData) { + assert(clipRRect.needsPainting); + + nWayCanvas.save(); + nWayCanvas.clipRRect( + clipRRect.clipRRect, clipRRect.clipBehavior != ui.Clip.hardEdge); + if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipRRect.paintBounds, null); + } + measureChildren(clipRRect); + if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + + @override + void visitOpacity(OpacityEngineLayer opacity, void childData) { + assert(opacity.needsPainting); + + final CkPaint paint = CkPaint(); + paint.color = ui.Color.fromARGB(opacity.alpha, 0, 0, 0); + + nWayCanvas.save(); + nWayCanvas.translate(opacity.offset.dx, opacity.offset.dy); + + final ui.Rect saveLayerBounds = opacity.paintBounds.shift(-opacity.offset); + + nWayCanvas.saveLayer(saveLayerBounds, paint); + measureChildren(opacity); + // Restore twice: once for the translate and once for the saveLayer. + nWayCanvas.restore(); + nWayCanvas.restore(); + } + + @override + void visitTransform(TransformEngineLayer transform, void childData) { + assert(transform.needsPainting); + + nWayCanvas.save(); + nWayCanvas.transform(transform.transform.storage); + measureChildren(transform); + nWayCanvas.restore(); + } + + @override + void visitOffset(OffsetEngineLayer offset, void childData) { + visitTransform(offset, null); + } + + @override + void visitImageFilter(ImageFilterEngineLayer imageFilter, void childData) { + assert(imageFilter.needsPainting); + final ui.Rect offsetPaintBounds = + imageFilter.paintBounds.shift(-imageFilter.offset); + nWayCanvas.save(); + nWayCanvas.translate(imageFilter.offset.dx, imageFilter.offset.dy); + nWayCanvas.clipRect(offsetPaintBounds, ui.ClipOp.intersect, false); + final CkPaint paint = CkPaint(); + paint.imageFilter = imageFilter.filter; + nWayCanvas.saveLayer(offsetPaintBounds, paint); + measureChildren(imageFilter); + nWayCanvas.restore(); + nWayCanvas.restore(); + } + + @override + void visitShaderMask(ShaderMaskEngineLayer shaderMask, void childData) { + assert(shaderMask.needsPainting); + + nWayCanvas.saveLayer(shaderMask.paintBounds, null); + measureChildren(shaderMask); + + nWayCanvas.restore(); + } + + @override + void visitPicture(PictureLayer picture, void childData) { + assert(picture.needsPainting); + + final CkCanvas pictureRecorderCanvas = + viewEmbedder.getMeasuringCanvasFor(picture); + + pictureRecorderCanvas.save(); + pictureRecorderCanvas.translate(picture.offset.dx, picture.offset.dy); + + pictureRecorderCanvas.drawPicture(picture.picture); + pictureRecorderCanvas.restore(); + + viewEmbedder.addPictureToUnoptimizedScene(picture); + } + + @override + void visitColorFilter(ColorFilterEngineLayer colorFilter, void childData) { + assert(colorFilter.needsPainting); + + final CkPaint paint = CkPaint(); + paint.colorFilter = colorFilter.filter; + + // We need to clip because if the ColorFilter affects transparent black, + // then it will fill the entire `cullRect` of the picture, ignoring the + // `paintBounds` passed to `saveLayer`. See: + // https://github.com/flutter/flutter/issues/88866 + nWayCanvas.save(); + + // TODO(hterkelsen): Only clip if the ColorFilter affects transparent black. + nWayCanvas.clipRect(colorFilter.paintBounds, ui.ClipOp.intersect, false); + + nWayCanvas.saveLayer(colorFilter.paintBounds, paint); + measureChildren(colorFilter); + nWayCanvas.restore(); + nWayCanvas.restore(); + } + + @override + void visitPlatformView(PlatformViewLayer platformView, void childData) { + // TODO(harryterkelsen): Warn if we are a child of a backdrop filter or + // shader mask. + viewEmbedder.compositeEmbeddedView(platformView.viewId); + } +} + +/// A layer visitor which paints the layer tree into one or more canvases. +/// +/// The canvases are the optimized canvases that were created when the view +/// embedder optimized the canvases after the measure step. +class PaintVisitor extends LayerVisitor { + PaintVisitor( + this.nWayCanvas, + HtmlViewEmbedder this.viewEmbedder, + ) : toImageCanvas = null; + + PaintVisitor.forToImage( + this.nWayCanvas, + this.toImageCanvas, + ) : viewEmbedder = null; + + /// A multi-canvas that applies clips, transforms, and opacity + /// operations to all canvases (root canvas and overlay canvases for the + /// platform views). + CkNWayCanvas nWayCanvas; + + /// A compositor for embedded HTML views. + final HtmlViewEmbedder? viewEmbedder; + + final List shaderMaskStack = []; + + final Map> picturesUnderShaderMask = + >{}; + + final CkCanvas? toImageCanvas; + + /// Calls [paint] on all child layers that need painting. + void paintChildren(ContainerLayer container) { + assert(container.needsPainting); + + for (final Layer layer in container.children) { + if (layer.needsPainting) { + layer.accept(this, null); + } + } + } + + @override + void visitRoot(RootLayer root, void childData) { + paintChildren(root); + } + + @override + void visitBackdropFilter( + BackdropFilterEngineLayer backdropFilter, void childData) { + final CkPaint paint = CkPaint()..blendMode = backdropFilter.blendMode; + + nWayCanvas.saveLayerWithFilter( + backdropFilter.paintBounds, backdropFilter.filter, paint); + paintChildren(backdropFilter); + nWayCanvas.restore(); + } + + @override + void visitClipPath(ClipPathEngineLayer clipPath, void childData) { + assert(clipPath.needsPainting); + + nWayCanvas.save(); + nWayCanvas.clipPath( + clipPath.clipPath, clipPath.clipBehavior != ui.Clip.hardEdge); + + if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipPath.paintBounds, null); + } + paintChildren(clipPath); + if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + + @override + void visitClipRect(ClipRectEngineLayer clipRect, void childData) { + assert(clipRect.needsPainting); + + nWayCanvas.save(); + nWayCanvas.clipRect( + clipRect.clipRect, + ui.ClipOp.intersect, + clipRect.clipBehavior != ui.Clip.hardEdge, + ); + if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipRect.clipRect, null); + } + paintChildren(clipRect); + if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + + @override + void visitClipRRect(ClipRRectEngineLayer clipRRect, void childData) { + assert(clipRRect.needsPainting); + + nWayCanvas.save(); + nWayCanvas.clipRRect( + clipRRect.clipRRect, clipRRect.clipBehavior != ui.Clip.hardEdge); + if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipRRect.paintBounds, null); + } + paintChildren(clipRRect); + if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + + @override + void visitOpacity(OpacityEngineLayer opacity, void childData) { + assert(opacity.needsPainting); + + final CkPaint paint = CkPaint(); + paint.color = ui.Color.fromARGB(opacity.alpha, 0, 0, 0); + + nWayCanvas.save(); + nWayCanvas.translate(opacity.offset.dx, opacity.offset.dy); + + final ui.Rect saveLayerBounds = opacity.paintBounds.shift(-opacity.offset); + + nWayCanvas.saveLayer(saveLayerBounds, paint); + paintChildren(opacity); + // Restore twice: once for the translate and once for the saveLayer. + nWayCanvas.restore(); + nWayCanvas.restore(); + } + + @override + void visitTransform(TransformEngineLayer transform, void childData) { + assert(transform.needsPainting); + + nWayCanvas.save(); + nWayCanvas.transform(transform.transform.storage); + paintChildren(transform); + nWayCanvas.restore(); + } + + @override + void visitOffset(OffsetEngineLayer offset, void childData) { + visitTransform(offset, null); + } + + @override + void visitImageFilter(ImageFilterEngineLayer imageFilter, void childData) { + assert(imageFilter.needsPainting); + final ui.Rect offsetPaintBounds = + imageFilter.paintBounds.shift(-imageFilter.offset); + nWayCanvas.save(); + nWayCanvas.translate(imageFilter.offset.dx, imageFilter.offset.dy); + nWayCanvas.clipRect(offsetPaintBounds, ui.ClipOp.intersect, false); + final CkPaint paint = CkPaint(); + paint.imageFilter = imageFilter.filter; + nWayCanvas.saveLayer(offsetPaintBounds, paint); + paintChildren(imageFilter); + nWayCanvas.restore(); + nWayCanvas.restore(); + } + + @override + void visitShaderMask(ShaderMaskEngineLayer shaderMask, void childData) { + assert(shaderMask.needsPainting); + + shaderMaskStack.add(shaderMask); + nWayCanvas.saveLayer(shaderMask.paintBounds, null); + paintChildren(shaderMask); + + final CkPaint paint = CkPaint(); + paint.shader = shaderMask.shader; + paint.blendMode = shaderMask.blendMode; + paint.filterQuality = shaderMask.filterQuality; + + late List canvasesToApplyShaderMask; + if (viewEmbedder != null) { + final Set canvases = {}; + for (final PictureLayer picture in picturesUnderShaderMask[shaderMask]!) { + canvases.add(viewEmbedder!.getOptimizedCanvasFor(picture)); + } + canvasesToApplyShaderMask = canvases.toList(); + } else { + canvasesToApplyShaderMask = [toImageCanvas!]; + } + + for (final CkCanvas canvas in canvasesToApplyShaderMask) { + canvas.save(); + canvas.translate(shaderMask.maskRect.left, shaderMask.maskRect.top); + + canvas.drawRect( + ui.Rect.fromLTWH( + 0, 0, shaderMask.maskRect.width, shaderMask.maskRect.height), + paint); + canvas.restore(); + } + nWayCanvas.restore(); + shaderMaskStack.removeLast(); + } + + @override + void visitPicture(PictureLayer picture, void childData) { + assert(picture.needsPainting); + + // For each shader mask this picture is a child of, record that it needs + // to have the shader mask applied to it. + for (final ShaderMaskEngineLayer shaderMask in shaderMaskStack) { + picturesUnderShaderMask.putIfAbsent(shaderMask, () => []); + picturesUnderShaderMask[shaderMask]!.add(picture); + } + + late CkCanvas pictureRecorderCanvas; + if (viewEmbedder != null) { + pictureRecorderCanvas = viewEmbedder!.getOptimizedCanvasFor(picture); + } else { + pictureRecorderCanvas = toImageCanvas!; + } + + pictureRecorderCanvas.save(); + pictureRecorderCanvas.translate(picture.offset.dx, picture.offset.dy); + + pictureRecorderCanvas.drawPicture(picture.picture); + pictureRecorderCanvas.restore(); + } + + @override + void visitColorFilter(ColorFilterEngineLayer colorFilter, void childData) { + assert(colorFilter.needsPainting); + + final CkPaint paint = CkPaint(); + paint.colorFilter = colorFilter.filter; + + // We need to clip because if the ColorFilter affects transparent black, + // then it will fill the entire `cullRect` of the picture, ignoring the + // `paintBounds` passed to `saveLayer`. See: + // https://github.com/flutter/flutter/issues/88866 + nWayCanvas.save(); + + // TODO(hterkelsen): Only clip if the ColorFilter affects transparent black. + nWayCanvas.clipRect(colorFilter.paintBounds, ui.ClipOp.intersect, false); + + nWayCanvas.saveLayer(colorFilter.paintBounds, paint); + paintChildren(colorFilter); + nWayCanvas.restore(); + nWayCanvas.restore(); + } + + @override + void visitPlatformView(PlatformViewLayer platformView, void childData) { + // Do nothing. The platform view was already measured and placed in the + // optimized rendering in the measure step. + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart index b98f969b1f..2d70f2329e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart @@ -156,108 +156,104 @@ 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 pictures, - List platformViews, + Iterable renderObjects, Map paramsForViews, ) { final Map cachedComputedRects = {}; - assert(pictures.length == platformViews.length + 1); final Rendering result = Rendering(); // The first picture is added to the rendering in a new render canvas. RenderingRenderCanvas tentativeCanvas = RenderingRenderCanvas(); - if (!pictures[0].cullRect.isEmpty) { - 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 = cachedComputedRects[platformViews[i]] = - computePlatformViewBounds(paramsForViews[platformViews[i]]!); + for (final SceneElement renderObject in renderObjects) { + if (renderObject is PlatformViewSceneElement) { + final int viewId = renderObject.viewId; + final RenderingPlatformView platformView = RenderingPlatformView(viewId); + if (PlatformViewManager.instance.isVisible(viewId)) { + final ui.Rect platformViewBounds = cachedComputedRects[viewId] = + computePlatformViewBounds(paramsForViews[viewId]!); - if (debugOverlayOptimizationBounds) { - platformView.debugComputedBounds = platformViewBounds; + if (debugOverlayOptimizationBounds) { + platformView.debugComputedBounds = platformViewBounds; + } + + // 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; + } + } + } + result.add(platformView); + } else if (renderObject is PictureSceneElement) { + final CkPicture scenePicture = renderObject.scenePicture!; + if (scenePicture.cullRect.isEmpty) { + continue; } - // If the platform view intersects with any pictures in the tentative canvas - // then add the tentative canvas to the rendering. + // 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(platformViewBounds).isEmpty) { - result.add(tentativeCanvas); - tentativeCanvas = RenderingRenderCanvas(); + if (!picture.cullRect.intersect(scenePicture.cullRect).isEmpty) { + tentativeCanvas.add(scenePicture); + addedToTentativeCanvas = true; break; } } - } - result.add(platformView); - - 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; } - } - 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]); + 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(scenePicture.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(scenePicture); + } else { + tentativeCanvas.add(scenePicture); + } + addedPictureToRendering = true; + break; } - 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; + } 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(scenePicture.cullRect).isEmpty) { + lastCanvasSeen.add(scenePicture); + 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 (!addedPictureToRendering) { + if (lastCanvasSeen != null) { + // Add it to the last canvas seen in the rendering, if any. + lastCanvasSeen.add(scenePicture); + } else { + tentativeCanvas.add(scenePicture); + } } } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 640e09562a..2e38674cd9 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -53,7 +53,7 @@ abstract class ViewRasterizer { // The [frameSize] may be slightly imprecise if the `devicePixelRatio` isn't // an integer. For example, is you zoom to 110% in Chrome on a Macbook, the // `devicePixelRatio` is `2.200000047683716`, so when the physical size is - // computed by multiplying the logical size by the devie pixel ratio, the + // computed by multiplying the logical size by the device pixel ratio, the // result is slightly imprecise as well. Nevertheless, the number should // be close to an integer, so round the frame size to be more precice. final BitmapSize bitmapSize = BitmapSize.fromSize(frameSize); @@ -61,14 +61,11 @@ abstract class ViewRasterizer { currentFrameSize = bitmapSize; prepareToDraw(); viewEmbedder.frameSize = currentFrameSize; - final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & currentFrameSize.toSize()); - final Frame compositorFrame = - context.acquireFrame(pictureRecorder.recordingCanvas!, viewEmbedder); + final Frame compositorFrame = context.acquireFrame(viewEmbedder); compositorFrame.raster(layerTree, ignoreRasterCache: true); - await viewEmbedder.submitFrame(pictureRecorder.endRecording()); + await viewEmbedder.submitFrame(); } /// Do some initialization to prepare to draw a frame. diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart index 99b540884a..cf71addabe 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -1152,8 +1152,7 @@ void testMain() { // 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. + // one. final LayerSceneBuilder sb5 = LayerSceneBuilder(); sb5.pushOffset(0, 0); sb5.addPicture( @@ -1181,9 +1180,7 @@ void testMain() { _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, _overlay, ]); @@ -1314,7 +1311,6 @@ void testMain() { _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, - _overlay, _platformView, _overlay, ]); diff --git a/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart b/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart index aeaef07ad0..8713314326 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart @@ -308,6 +308,24 @@ Future testMain() async { await renderScene(sceneBuilder.build()); await matchGoldenFile('scene_builder_color_filter.png', region: region); }); + + test('overlapping pictures in opacity layer', () async { + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder.pushOpacity(128); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawCircle(const ui.Offset(100, 150), 100, + ui.Paint()..color = const ui.Color(0xFFFF0000)); + })); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawCircle(const ui.Offset(200, 150), 100, + ui.Paint()..color = const ui.Color(0xFFFF0000)); + })); + sceneBuilder.pop(); + + await renderScene(sceneBuilder.build()); + await matchGoldenFile('scene_builder_overlapping_pictures_in_opacity.png', + region: region); + }); }); }