From 746149965de9d31687096a857f58bec6fd71ec2b Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:47:18 -0700 Subject: [PATCH] Reland "[canvaskit] Further improve overlay optimization by splitting pictures" (#55402) 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). This is a reland of https://github.com/flutter/engine/pull/54878 with a fix for scenes with pictures that are eventually entirely clipped out. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- ci/licenses_golden/licenses_flutter | 2 + lib/web_ui/lib/src/engine.dart | 1 + .../src/engine/canvaskit/embedded_views.dart | 196 +++-- .../lib/src/engine/canvaskit/layer.dart | 453 ++---------- .../lib/src/engine/canvaskit/layer_tree.dart | 56 +- .../src/engine/canvaskit/layer_visitor.dart | 674 ++++++++++++++++++ .../canvaskit/overlay_scene_optimizer.dart | 152 ++-- .../lib/src/engine/canvaskit/rasterizer.dart | 9 +- .../test/canvaskit/embedded_views_test.dart | 6 +- lib/web_ui/test/ui/scene_builder_test.dart | 256 ++++--- 10 files changed, 1149 insertions(+), 656 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index f6cdc665d4706..6dae3b2eb7593 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/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/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index cdf615a388617..f50b7cf78c73e 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/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/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index a99216b763748..f51456f6a546b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/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!); } - void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) { - final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); - _context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder); + /// 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) { // 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,57 @@ 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(); + if (scenePicture.cullRect.isEmpty) { + element.picture.isCulled = true; + } + 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 +426,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 +448,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 +958,45 @@ class MutatorsStack extends Iterable { Iterable get reversed => _mutators; } -/// 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 = - []; +sealed class SceneElement {} - /// Picture recorders which were actually used in the paint phase. - /// - /// This is a subset of [_pictureRecordersCreatedDuringPreroll]. - final List pictureRecorders = []; +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; +} - /// The number of platform views in this frame. - int viewCount = 0; +class PlatformViewSceneElement extends SceneElement { + PlatformViewSceneElement(this.viewId); + + final int viewId; +} + +/// The state for the current frame. +class EmbedderFrameContext { + /// Picture recorders which were created d the final bounds of the picture in the scene. + final Map measuringPictureRecorders = + {}; + + /// List of picture recorders and platform view ids in the order they were + /// painted. + final List sceneElements = []; + + /// 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/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/lib/web_ui/lib/src/engine/canvaskit/layer.dart index 010fc012786d3..79792c4cdb2f7 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/layer.dart +++ b/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); // 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) { + visitor.visitRoot(this); } } 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; - - @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final ui.Rect childBounds = prerollChildren(prerollContext, matrix); - paintBounds = childBounds.expandToInclude(prerollContext.cullRect); - } + final ui.ImageFilter filter; + final ui.BlendMode blendMode; @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) { + visitor.visitBackdropFilter(this); } // 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) { + visitor.visitClipPath(this); } } /// 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; - - @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(); - } + final ui.Rect clipRect; + final ui.Clip clipBehavior; @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) { + visitor.visitClipRect(this); } } /// 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; - - @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(); - } + final ui.RRect clipRRect; + final ui.Clip? clipBehavior; @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) { + visitor.visitClipRRect(this); } } /// A layer that paints its children with the given opacity. class OpacityEngineLayer extends ContainerLayer implements ui.OpacityEngineLayer { - OpacityEngineLayer(this._alpha, this._offset); - - final int _alpha; - final ui.Offset _offset; + OpacityEngineLayer(this.alpha, this.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); - } + final int alpha; + final ui.Offset offset; @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) { + visitor.visitOpacity(this); } } /// 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; - - @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(); - } + final Matrix4 transform; @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas.transform(_transform.storage); - paintChildren(paintContext); - paintContext.internalNodesCanvas.restore(); + void accept(LayerVisitor visitor) { + visitor.visitTransform(this); } } @@ -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) { + visitor.visitOffset(this); + } } /// 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; - - @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(); - } + final ui.Offset offset; + final ui.ImageFilter filter; @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) { + visitor.visitImageFilter(this); } // 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) { + visitor.visitShaderMask(this); } } @@ -501,21 +216,17 @@ class PictureLayer extends Layer { /// A hint to the compositor that this picture is likely to change. final bool willChange; + /// Whether or not this picture is culled in the final scene. We compute this + /// when we optimize the scene. + bool isCulled = false; + @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - paintBounds = picture.cullRect.shift(offset); + void accept(LayerVisitor visitor) { + visitor.visitPicture(this); } @override - void paint(PaintContext paintContext) { - assert(needsPainting); - - paintContext.leafNodesCanvas!.save(); - paintContext.leafNodesCanvas!.translate(offset.dx, offset.dy); - - paintContext.leafNodesCanvas!.drawPicture(picture); - paintContext.leafNodesCanvas!.restore(); - } + bool get needsPainting => super.needsPainting && !isCulled; } /// A layer which contains a [ui.ColorFilter]. @@ -526,26 +237,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) { + visitor.visitColorFilter(this); } } @@ -559,27 +252,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) { + visitor.visitPlatformView(this); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart index 2aca00f35dd3c..0a6d57ae467c8 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart @@ -6,10 +6,10 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show kProfileApplyFrame, kProfilePrerollFrame; import '../profiler.dart'; -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 +31,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); + } + + /// 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); + } } /// Paints the layer tree into the given [frame]. @@ -44,18 +58,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); } } @@ -65,15 +76,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); 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); } return recorder.endRecording(); } @@ -81,10 +92,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 +104,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 +120,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/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart new file mode 100644 index 0000000000000..8c7631a7f1051 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart @@ -0,0 +1,674 @@ +// 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); + void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter); + void visitClipPath(ClipPathEngineLayer clipPath); + void visitClipRect(ClipRectEngineLayer clipRect); + void visitClipRRect(ClipRRectEngineLayer clipRRect); + void visitOpacity(OpacityEngineLayer opacity); + void visitTransform(TransformEngineLayer transform); + void visitOffset(OffsetEngineLayer offset); + void visitImageFilter(ImageFilterEngineLayer imageFilter); + void visitShaderMask(ShaderMaskEngineLayer shaderMask); + void visitPicture(PictureLayer picture); + void visitColorFilter(ColorFilterEngineLayer colorFilter); + void visitPlatformView(PlatformViewLayer platformView); +} + +/// 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) { + ui.Rect childPaintBounds = ui.Rect.zero; + for (final Layer layer in layer.children) { + layer.accept(this); + if (childPaintBounds.isEmpty) { + childPaintBounds = layer.paintBounds; + } else if (!layer.paintBounds.isEmpty) { + childPaintBounds = childPaintBounds.expandToInclude(layer.paintBounds); + } + } + return childPaintBounds; + } + + void prerollContainerLayer(ContainerLayer container) { + container.paintBounds = prerollChildren(container); + } + + @override + void visitRoot(RootLayer root) { + prerollContainerLayer(root); + } + + @override + void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter) { + final ui.Rect childBounds = prerollChildren(backdropFilter); + backdropFilter.paintBounds = childBounds.expandToInclude(cullRect); + } + + @override + void visitClipPath(ClipPathEngineLayer clipPath) { + mutatorsStack.pushClipPath(clipPath.clipPath); + final ui.Rect childPaintBounds = prerollChildren(clipPath); + final ui.Rect clipBounds = clipPath.clipPath.getBounds(); + if (childPaintBounds.overlaps(clipBounds)) { + clipPath.paintBounds = childPaintBounds.intersect(clipBounds); + } + mutatorsStack.pop(); + } + + @override + void visitClipRRect(ClipRRectEngineLayer clipRRect) { + mutatorsStack.pushClipRRect(clipRRect.clipRRect); + final ui.Rect childPaintBounds = prerollChildren(clipRRect); + if (childPaintBounds.overlaps(clipRRect.clipRRect.outerRect)) { + clipRRect.paintBounds = + childPaintBounds.intersect(clipRRect.clipRRect.outerRect); + } + mutatorsStack.pop(); + } + + @override + void visitClipRect(ClipRectEngineLayer clipRect) { + mutatorsStack.pushClipRect(clipRect.clipRect); + final ui.Rect childPaintBounds = prerollChildren(clipRect); + if (childPaintBounds.overlaps(clipRect.clipRect)) { + clipRect.paintBounds = childPaintBounds.intersect(clipRect.clipRect); + } + mutatorsStack.pop(); + } + + @override + void visitColorFilter(ColorFilterEngineLayer colorFilter) { + prerollContainerLayer(colorFilter); + } + + @override + void visitImageFilter(ImageFilterEngineLayer imageFilter) { + 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); + 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) { + visitTransform(offset); + } + + @override + void visitOpacity(OpacityEngineLayer opacity) { + mutatorsStack.pushTransform( + Matrix4.translationValues(opacity.offset.dx, opacity.offset.dy, 0.0)); + mutatorsStack.pushOpacity(opacity.alpha); + prerollContainerLayer(opacity); + mutatorsStack.pop(); + mutatorsStack.pop(); + opacity.paintBounds = + opacity.paintBounds.translate(opacity.offset.dx, opacity.offset.dy); + } + + @override + void visitPicture(PictureLayer picture) { + picture.paintBounds = picture.picture.cullRect.shift(picture.offset); + // The picture may have been culled on a previous frame, but has since + // scrolled back into the clip region. Reset the `isCulled` flag. + picture.isCulled = false; + viewEmbedder?.prerollPicture(picture); + } + + @override + void visitPlatformView(PlatformViewLayer platformView) { + 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) { + shaderMask.paintBounds = prerollChildren(shaderMask); + } + + @override + void visitTransform(TransformEngineLayer transform) { + mutatorsStack.pushTransform(transform.transform); + final ui.Rect childPaintBounds = prerollChildren(transform); + 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); + } + } + } + + @override + void visitRoot(RootLayer root) { + measureChildren(root); + } + + @override + void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter) { + measureChildren(backdropFilter); + } + + @override + void visitClipPath(ClipPathEngineLayer clipPath) { + 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) { + 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) { + 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) { + 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) { + assert(transform.needsPainting); + + nWayCanvas.save(); + nWayCanvas.transform(transform.transform.storage); + measureChildren(transform); + nWayCanvas.restore(); + } + + @override + void visitOffset(OffsetEngineLayer offset) { + visitTransform(offset); + } + + @override + void visitImageFilter(ImageFilterEngineLayer imageFilter) { + 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) { + assert(shaderMask.needsPainting); + + nWayCanvas.saveLayer(shaderMask.paintBounds, null); + measureChildren(shaderMask); + + nWayCanvas.restore(); + } + + @override + void visitPicture(PictureLayer picture) { + 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) { + 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) { + // 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); + } + } + } + + @override + void visitRoot(RootLayer root) { + paintChildren(root); + } + + @override + void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter) { + final CkPaint paint = CkPaint()..blendMode = backdropFilter.blendMode; + + nWayCanvas.saveLayerWithFilter( + backdropFilter.paintBounds, backdropFilter.filter, paint); + paintChildren(backdropFilter); + nWayCanvas.restore(); + } + + @override + void visitClipPath(ClipPathEngineLayer clipPath) { + 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) { + 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) { + 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) { + 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) { + assert(transform.needsPainting); + + nWayCanvas.save(); + nWayCanvas.transform(transform.transform.storage); + paintChildren(transform); + nWayCanvas.restore(); + } + + @override + void visitOffset(OffsetEngineLayer offset) { + visitTransform(offset); + } + + @override + void visitImageFilter(ImageFilterEngineLayer imageFilter) { + 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) { + 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) { + 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) { + 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) { + // Do nothing. The platform view was already measured and placed in the + // optimized rendering in the measure step. + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart b/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart index b98f969b1fbe3..2d70f2329e1e2 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart +++ b/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; + } + + // 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. - // If the platform view intersects with any pictures in the tentative canvas - // then add the tentative canvas to the rendering. + // 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/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 640e09562ad7a..2e38674cd95d7 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/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/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index 99b540884a892..cf71addabe271 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/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/lib/web_ui/test/ui/scene_builder_test.dart b/lib/web_ui/test/ui/scene_builder_test.dart index aeaef07ad0c5a..8ebde3a3156e1 100644 --- a/lib/web_ui/test/ui/scene_builder_test.dart +++ b/lib/web_ui/test/ui/scene_builder_test.dart @@ -32,14 +32,12 @@ Future testMain() async { sceneBuilder.pushOffset(150, 150); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawCircle( - ui.Offset.zero, - 50, - ui.Paint()..color = const ui.Color(0xFF00FF00) - ); + ui.Offset.zero, 50, ui.Paint()..color = const ui.Color(0xFF00FF00)); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_centered_circle.png', region: region); + await matchGoldenFile('scene_builder_centered_circle.png', + region: region); }); test('Test transform layer', () async { @@ -54,93 +52,83 @@ Future testMain() async { sceneBuilder.pushTransform(transform.toFloat64()); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRRect( - ui.RRect.fromRectAndRadius( - ui.Rect.fromCircle(center: ui.Offset.zero, radius: 50), - const ui.Radius.circular(10) - ), - ui.Paint()..color = const ui.Color(0xFF0000FF) - ); + ui.RRect.fromRectAndRadius( + ui.Rect.fromCircle(center: ui.Offset.zero, radius: 50), + const ui.Radius.circular(10)), + ui.Paint()..color = const ui.Color(0xFF0000FF)); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_rotated_rounded_square.png', region: region); + await matchGoldenFile('scene_builder_rotated_rounded_square.png', + region: region); }); test('Test clipRect layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 150, 150)); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle( - const ui.Offset(150, 150), - 50, - ui.Paint()..color = const ui.Color(0xFFFF0000) - ); + canvas.drawCircle(const ui.Offset(150, 150), 50, + ui.Paint()..color = const ui.Color(0xFFFF0000)); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_circle_clip_rect.png', region: region); + await matchGoldenFile('scene_builder_circle_clip_rect.png', + region: region); }); test('Test clipRRect layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder.pushClipRRect(ui.RRect.fromRectAndRadius( - const ui.Rect.fromLTRB(0, 0, 150, 150), - const ui.Radius.circular(25), - ), clipBehavior: ui.Clip.antiAlias); + sceneBuilder.pushClipRRect( + ui.RRect.fromRectAndRadius( + const ui.Rect.fromLTRB(0, 0, 150, 150), + const ui.Radius.circular(25), + ), + clipBehavior: ui.Clip.antiAlias); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle( - const ui.Offset(150, 150), - 50, - ui.Paint()..color = const ui.Color(0xFFFF00FF) - ); + canvas.drawCircle(const ui.Offset(150, 150), 50, + ui.Paint()..color = const ui.Color(0xFFFF00FF)); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_circle_clip_rrect.png', region: region); + await matchGoldenFile('scene_builder_circle_clip_rrect.png', + region: region); }); test('Test clipPath layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); final ui.Path path = ui.Path(); - path.addOval(ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 60)); + path.addOval( + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 60)); sceneBuilder.pushClipPath(path); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRect( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), - ui.Paint()..color = const ui.Color(0xFF00FFFF) - ); + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), + ui.Paint()..color = const ui.Color(0xFF00FFFF)); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_rectangle_clip_circular_path.png', region: region); + await matchGoldenFile('scene_builder_rectangle_clip_circular_path.png', + region: region); }); test('Test opacity layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRect( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), - ui.Paint()..color = const ui.Color(0xFF00FF00) - ); + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), + ui.Paint()..color = const ui.Color(0xFF00FF00)); })); sceneBuilder.pushOpacity(0x7F, offset: const ui.Offset(150, 150)); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { final ui.Paint paint = ui.Paint()..color = const ui.Color(0xFFFF0000); - canvas.drawCircle( - const ui.Offset(-25, 0), - 50, - paint - ); - canvas.drawCircle( - const ui.Offset(25, 0), - 50, - paint - ); + canvas.drawCircle(const ui.Offset(-25, 0), 50, paint); + canvas.drawCircle(const ui.Offset(25, 0), 50, paint); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_opacity_circles_on_square.png', region: region); + await matchGoldenFile('scene_builder_opacity_circles_on_square.png', + region: region); }); test('shader mask layer', () async { @@ -148,48 +136,39 @@ Future testMain() async { sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { final ui.Paint paint = ui.Paint()..color = const ui.Color(0xFFFF0000); - canvas.drawCircle( - const ui.Offset(125, 150), - 50, - paint - ); - canvas.drawCircle( - const ui.Offset(175, 150), - 50, - paint - ); + canvas.drawCircle(const ui.Offset(125, 150), 50, paint); + canvas.drawCircle(const ui.Offset(175, 150), 50, paint); })); final ui.Shader shader = ui.Gradient.linear( - ui.Offset.zero, - const ui.Offset(50, 50), [ - const ui.Color(0xFFFFFFFF), - const ui.Color(0x00000000), - ]); - sceneBuilder.pushShaderMask( - shader, - const ui.Rect.fromLTRB(125, 125, 175, 175), - ui.BlendMode.srcATop - ); + ui.Offset.zero, const ui.Offset(50, 50), [ + const ui.Color(0xFFFFFFFF), + const ui.Color(0x00000000), + ]); + sceneBuilder.pushShaderMask(shader, + const ui.Rect.fromLTRB(125, 125, 175, 175), ui.BlendMode.srcATop); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRect( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), - ui.Paint()..color = const ui.Color(0xFF00FF00) - ); + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), + ui.Paint()..color = const ui.Color(0xFF00FF00)); })); await renderScene(sceneBuilder.build()); await matchGoldenFile('scene_builder_shader_mask.png', region: region); - }, skip: isFirefox && isHtml); // https://github.com/flutter/flutter/issues/86623 + }, + skip: isFirefox && + isHtml); // https://github.com/flutter/flutter/issues/86623 test('backdrop filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { // Create a red and blue checkerboard pattern - final ui.Paint redPaint = ui.Paint()..color = const ui.Color(0xFFFF0000); - final ui.Paint bluePaint = ui.Paint()..color = const ui.Color(0xFF0000FF); + final ui.Paint redPaint = ui.Paint() + ..color = const ui.Color(0xFFFF0000); + final ui.Paint bluePaint = ui.Paint() + ..color = const ui.Color(0xFF0000FF); for (double y = 0; y < 300; y += 10) { for (double x = 0; x < 300; x += 10) { final ui.Paint paint = ((x + y) % 20 == 0) ? redPaint : bluePaint; @@ -204,15 +183,13 @@ Future testMain() async { )); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle( - const ui.Offset(150, 150), - 50, - ui.Paint()..color = const ui.Color(0xFF00FF00) - ); + canvas.drawCircle(const ui.Offset(150, 150), 50, + ui.Paint()..color = const ui.Color(0xFF00FF00)); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_backdrop_filter.png', region: region); + await matchGoldenFile('scene_builder_backdrop_filter.png', + region: region); }); test('empty backdrop filter layer with clip', () async { @@ -223,8 +200,10 @@ Future testMain() async { sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { // Create a red and blue checkerboard pattern - final ui.Paint redPaint = ui.Paint()..color = const ui.Color(0xFFFF0000); - final ui.Paint bluePaint = ui.Paint()..color = const ui.Color(0xFF0000FF); + final ui.Paint redPaint = ui.Paint() + ..color = const ui.Color(0xFFFF0000); + final ui.Paint bluePaint = ui.Paint() + ..color = const ui.Color(0xFF0000FF); for (double y = 0; y < 300; y += 10) { for (double x = 0; x < 300; x += 10) { final ui.Paint paint = ((x + y) % 20 == 0) ? redPaint : bluePaint; @@ -240,7 +219,8 @@ Future testMain() async { sigmaY: 3.0, )); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_empty_backdrop_filter_with_clip.png', region: region); + await matchGoldenFile('scene_builder_empty_backdrop_filter_with_clip.png', + region: region); }); test('image filter layer', () async { @@ -251,11 +231,8 @@ Future testMain() async { )); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle( - const ui.Offset(150, 150), - 50, - ui.Paint()..color = const ui.Color(0xFF00FF00) - ); + canvas.drawCircle(const ui.Offset(150, 150), 50, + ui.Paint()..color = const ui.Color(0xFF00FF00)); })); await renderScene(sceneBuilder.build()); @@ -290,24 +267,111 @@ Future testMain() async { test('color filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); const ui.ColorFilter sepia = ui.ColorFilter.matrix([ - 0.393, 0.769, 0.189, 0, 0, - 0.349, 0.686, 0.168, 0, 0, - 0.272, 0.534, 0.131, 0, 0, - 0, 0, 0, 1, 0, + 0.393, + 0.769, + 0.189, + 0, + 0, + 0.349, + 0.686, + 0.168, + 0, + 0, + 0.272, + 0.534, + 0.131, + 0, + 0, + 0, + 0, + 0, + 1, + 0, ]); sceneBuilder.pushColorFilter(sepia); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle( - const ui.Offset(150, 150), - 50, - ui.Paint()..color = const ui.Color(0xFF00FF00) - ); + canvas.drawCircle(const ui.Offset(150, 150), 50, + ui.Paint()..color = const ui.Color(0xFF00FF00)); })); 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); + }); + + test('picture clipped out in final scene', () async { + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300)); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawCircle(const ui.Offset(50, 150), 50, + ui.Paint()..color = const ui.Color(0xFFFF0000)); + })); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawCircle(const ui.Offset(200, 150), 50, + ui.Paint()..color = const ui.Color(0xFFFF0000)); + })); + sceneBuilder.pop(); + + await renderScene(sceneBuilder.build()); + await matchGoldenFile('scene_builder_picture_clipped_out.png', + region: region); + }); + + test('picture clipped but scrolls back in', () async { + // Frame 1: Clip out the right circle + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300)); + // Save this offsetLayer to add back in so we are using the same + // picture layers on the next scene. + final ui.OffsetEngineLayer offsetLayer = sceneBuilder.pushOffset(0, 0); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawCircle(const ui.Offset(50, 150), 50, + ui.Paint()..color = const ui.Color(0xFFFF0000)); + })); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawCircle(const ui.Offset(200, 150), 50, + ui.Paint()..color = const ui.Color(0xFFFF0000)); + })); + sceneBuilder.pop(); + sceneBuilder.pop(); + await renderScene(sceneBuilder.build()); + + // Frame 2: Clip out the left circle + final ui.SceneBuilder sceneBuilder2 = ui.SceneBuilder(); + sceneBuilder2.pushClipRect(const ui.Rect.fromLTRB(150, 0, 300, 300)); + sceneBuilder2.addRetained(offsetLayer); + sceneBuilder2.pop(); + await renderScene(sceneBuilder2.build()); + + // Frame 3: Clip out the right circle again + final ui.SceneBuilder sceneBuilder3 = ui.SceneBuilder(); + sceneBuilder3.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300)); + sceneBuilder3.addRetained(offsetLayer); + sceneBuilder3.pop(); + await renderScene(sceneBuilder3.build()); + + await matchGoldenFile( + 'scene_builder_picture_clipped_out_then_clipped_in.png', + region: region); + }); }); }