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); + }); }); }