From 0a20de5ad44c4dfb0dde6739a11efffaf874d82a Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Fri, 13 Sep 2024 10:55:07 -0700 Subject: [PATCH 1/5] Reland [skwasm] Scene builder optimizations for platform view placement This is an attempt to reland the overlay optimization for skwasm and fixing the golden diffs from the framework tests. Original PR description: This PR refactors the scene builder's logic in order to more aggressively merge flutter content and platform view content together. This essentially covers the case discussed in this flutter issue: https://github.com/flutter/flutter/issues/149863 This optimization ensures that each picture or platform view is applied to the lowest possible slice in the scene, which avoids the proliferation of redundant slices and overlays in the scene. --- lib/web_ui/lib/src/engine/color_filter.dart | 3 + lib/web_ui/lib/src/engine/layers.dart | 517 +++++++++++------- lib/web_ui/lib/src/engine/scene_builder.dart | 275 ++++++++-- lib/web_ui/lib/src/engine/scene_painting.dart | 6 + lib/web_ui/lib/src/engine/scene_view.dart | 126 +++-- .../engine/skwasm/skwasm_impl/filters.dart | 25 + .../test/engine/scene_builder_test.dart | 174 +++--- .../test/engine/scene_builder_utils.dart | 6 +- lib/web_ui/test/engine/scene_view_test.dart | 8 +- 9 files changed, 767 insertions(+), 373 deletions(-) diff --git a/lib/web_ui/lib/src/engine/color_filter.dart b/lib/web_ui/lib/src/engine/color_filter.dart index 83a7ca27e2cc0..a74145c08bd78 100644 --- a/lib/web_ui/lib/src/engine/color_filter.dart +++ b/lib/web_ui/lib/src/engine/color_filter.dart @@ -135,4 +135,7 @@ class EngineColorFilter implements SceneImageFilter, ui.ColorFilter { return 'ColorFilter.srgbToLinearGamma()'; } } + + @override + Matrix4? get transform => null; } diff --git a/lib/web_ui/lib/src/engine/layers.dart b/lib/web_ui/lib/src/engine/layers.dart index f3658ddee3c4c..380194978bad8 100644 --- a/lib/web_ui/lib/src/engine/layers.dart +++ b/lib/web_ui/lib/src/engine/layers.dart @@ -5,15 +5,51 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; -import 'package:ui/src/engine/scene_painting.dart'; -import 'package:ui/src/engine/vector_math.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -class EngineRootLayer with PictureEngineLayer {} +class EngineRootLayer with PictureEngineLayer { + @override + final NoopOperation operation = const NoopOperation(); + + @override + EngineRootLayer emptyClone() => EngineRootLayer(); +} + +class NoopOperation implements LayerOperation { + const NoopOperation(); + + @override + PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); + + @override + ui.Rect mapRect(ui.Rect contentRect) => contentRect; + + @override + void pre(SceneCanvas canvas) { + canvas.save(); + } + + @override + void post(SceneCanvas canvas) { + canvas.restore(); + } + + @override + bool get shouldDrawIfEmpty => false; +} class BackdropFilterLayer with PictureEngineLayer - implements ui.BackdropFilterEngineLayer {} + implements ui.BackdropFilterEngineLayer { + BackdropFilterLayer(this.operation); + + @override + final LayerOperation operation; + + @override + BackdropFilterLayer emptyClone() => BackdropFilterLayer(operation); +} class BackdropFilterOperation implements LayerOperation { BackdropFilterOperation(this.filter, this.mode); @@ -24,12 +60,12 @@ class BackdropFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayerWithFilter(contentRect, ui.Paint()..blendMode = mode, filter); + void pre(SceneCanvas canvas) { + canvas.saveLayerWithFilter(null, ui.Paint()..blendMode = mode, filter); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -44,7 +80,15 @@ class BackdropFilterOperation implements LayerOperation { class ClipPathLayer with PictureEngineLayer - implements ui.ClipPathEngineLayer {} + implements ui.ClipPathEngineLayer { + ClipPathLayer(this.operation); + + @override + final ClipPathOperation operation; + + @override + ClipPathLayer emptyClone() => ClipPathLayer(operation); +} class ClipPathOperation implements LayerOperation { ClipPathOperation(this.path, this.clip); @@ -55,16 +99,16 @@ class ClipPathOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(path.getBounds()); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(path.getBounds(), ui.Paint()); + canvas.saveLayer(null, ui.Paint()); } } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -82,7 +126,15 @@ class ClipPathOperation implements LayerOperation { class ClipRectLayer with PictureEngineLayer - implements ui.ClipRectEngineLayer {} + implements ui.ClipRectEngineLayer { + ClipRectLayer(this.operation); + + @override + final ClipRectOperation operation; + + @override + ClipRectLayer emptyClone() => ClipRectLayer(operation); +} class ClipRectOperation implements LayerOperation { const ClipRectOperation(this.rect, this.clip); @@ -93,7 +145,7 @@ class ClipRectOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rect); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.clipRect(rect, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -102,7 +154,7 @@ class ClipRectOperation implements LayerOperation { } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -120,7 +172,15 @@ class ClipRectOperation implements LayerOperation { class ClipRRectLayer with PictureEngineLayer - implements ui.ClipRRectEngineLayer {} + implements ui.ClipRRectEngineLayer { + ClipRRectLayer(this.operation); + + @override + final ClipRRectOperation operation; + + @override + ClipRRectLayer emptyClone() => ClipRRectLayer(operation); +} class ClipRRectOperation implements LayerOperation { const ClipRRectOperation(this.rrect, this.clip); @@ -131,7 +191,7 @@ class ClipRRectOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rrect.outerRect); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.clipRRect(rrect, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -140,7 +200,7 @@ class ClipRRectOperation implements LayerOperation { } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -158,7 +218,15 @@ class ClipRRectOperation implements LayerOperation { class ColorFilterLayer with PictureEngineLayer - implements ui.ColorFilterEngineLayer {} + implements ui.ColorFilterEngineLayer { + ColorFilterLayer(this.operation); + + @override + final ColorFilterOperation operation; + + @override + ColorFilterLayer emptyClone() => ColorFilterLayer(operation); +} class ColorFilterOperation implements LayerOperation { ColorFilterOperation(this.filter); @@ -168,12 +236,12 @@ class ColorFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayer(contentRect, ui.Paint()..colorFilter = filter); + void pre(SceneCanvas canvas) { + canvas.saveLayer(null, ui.Paint()..colorFilter = filter); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -186,7 +254,15 @@ class ColorFilterOperation implements LayerOperation { class ImageFilterLayer with PictureEngineLayer - implements ui.ImageFilterEngineLayer {} + implements ui.ImageFilterEngineLayer { + ImageFilterLayer(this.operation); + + @override + final ImageFilterOperation operation; + + @override + ImageFilterLayer emptyClone() => ImageFilterLayer(operation); +} class ImageFilterOperation implements LayerOperation { ImageFilterOperation(this.filter, this.offset); @@ -197,17 +273,16 @@ class ImageFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => filter.filterBounds(contentRect); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { if (offset != ui.Offset.zero) { canvas.save(); canvas.translate(offset.dx, offset.dy); } - final ui.Rect adjustedContentRect = filter.filterBounds(contentRect); - canvas.saveLayer(adjustedContentRect, ui.Paint()..imageFilter = filter); + canvas.saveLayer(null, ui.Paint()..imageFilter = filter); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (offset != ui.Offset.zero) { canvas.restore(); } @@ -216,13 +291,22 @@ class ImageFilterOperation implements LayerOperation { @override PlatformViewStyling createPlatformViewStyling() { + PlatformViewStyling styling = const PlatformViewStyling(); if (offset != ui.Offset.zero) { - return PlatformViewStyling( + styling = PlatformViewStyling( position: PlatformViewPosition.offset(offset) ); - } else { - return const PlatformViewStyling(); } + final Matrix4? transform = filter.transform; + if (transform != null) { + styling = PlatformViewStyling.combine( + styling, + PlatformViewStyling( + position: PlatformViewPosition.transform(transform), + ), + ); + } + return const PlatformViewStyling(); } @override @@ -231,7 +315,15 @@ class ImageFilterOperation implements LayerOperation { class OffsetLayer with PictureEngineLayer - implements ui.OffsetEngineLayer {} + implements ui.OffsetEngineLayer { + OffsetLayer(this.operation); + + @override + final OffsetOperation operation; + + @override + OffsetLayer emptyClone() => OffsetLayer(operation); +} class OffsetOperation implements LayerOperation { OffsetOperation(this.dx, this.dy); @@ -242,13 +334,13 @@ class OffsetOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(ui.Offset(dx, dy)); @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.translate(dx, dy); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -263,7 +355,15 @@ class OffsetOperation implements LayerOperation { class OpacityLayer with PictureEngineLayer - implements ui.OpacityEngineLayer {} + implements ui.OpacityEngineLayer { + OpacityLayer(this.operation); + + @override + final OpacityOperation operation; + + @override + OpacityLayer emptyClone() => OpacityLayer(operation); +} class OpacityOperation implements LayerOperation { OpacityOperation(this.alpha, this.offset); @@ -274,20 +374,19 @@ class OpacityOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(offset); @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { + void pre(SceneCanvas canvas) { if (offset != ui.Offset.zero) { canvas.save(); canvas.translate(offset.dx, offset.dy); - cullRect = cullRect.shift(-offset); } canvas.saveLayer( - cullRect, + null, ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0) ); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); if (offset != ui.Offset.zero) { canvas.restore(); @@ -306,7 +405,15 @@ class OpacityOperation implements LayerOperation { class TransformLayer with PictureEngineLayer - implements ui.TransformEngineLayer {} + implements ui.TransformEngineLayer { + TransformLayer(this.operation); + + @override + final TransformOperation operation; + + @override + TransformLayer emptyClone() => TransformLayer(operation); +} class TransformOperation implements LayerOperation { TransformOperation(this.transform); @@ -319,13 +426,13 @@ class TransformOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => matrix.transformRect(contentRect); @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.transform(transform); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -340,7 +447,15 @@ class TransformOperation implements LayerOperation { class ShaderMaskLayer with PictureEngineLayer - implements ui.ShaderMaskEngineLayer {} + implements ui.ShaderMaskEngineLayer { + ShaderMaskLayer(this.operation); + + @override + final ShaderMaskOperation operation; + + @override + ShaderMaskLayer emptyClone() => ShaderMaskLayer(operation); +} class ShaderMaskOperation implements LayerOperation { ShaderMaskOperation(this.shader, this.maskRect, this.blendMode); @@ -352,15 +467,15 @@ class ShaderMaskOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.saveLayer( - contentRect, + null, ui.Paint(), ); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.save(); canvas.translate(maskRect.left, maskRect.top); canvas.drawRect( @@ -389,47 +504,43 @@ class PlatformView { final ui.Rect bounds; final PlatformViewStyling styling; -} -sealed class LayerSlice { - void dispose(); + @override + String toString() { + return 'PlatformView(viewId: $viewId, bounds: $bounds, styling: $styling)'; + } } -// A slice that contains one or more platform views to be rendered. -class PlatformViewSlice implements LayerSlice { - PlatformViewSlice(this.views, this.occlusionRect); +class LayerSlice { + LayerSlice(this.picture, this.platformViews); - List views; + // The picture of native flutter content to be rendered + ScenePicture picture; - // A conservative estimate of what area platform views in this slice may cover. - // This is expressed in the coordinate space of the parent. - ui.Rect? occlusionRect; + // Platform views to be placed on top of the flutter content. + final List platformViews; - @override - void dispose() {} + void dispose() { + picture.dispose(); + } } -// A slice that contains flutter content to be rendered int he form of a single -// ScenePicture. -class PictureSlice implements LayerSlice { - PictureSlice(this.picture); +mixin PictureEngineLayer implements ui.EngineLayer { + // Each layer is represented as a series of "slices" which contain flutter content + // with platform views on top. This is ordered from bottommost to topmost. + List slices = []; - ScenePicture picture; + List drawCommands = []; + PlatformViewStyling platformViewStyling = const PlatformViewStyling(); - @override - void dispose() => picture.dispose(); -} + LayerOperation get operation; -mixin PictureEngineLayer implements ui.EngineLayer { - // Each layer is represented as a series of "slices" which contain either - // flutter content or platform views. Slices in this list are ordered from - // bottom to top. - List slices = []; + PictureEngineLayer emptyClone(); @override void dispose() { - for (final LayerSlice slice in slices) { - slice.dispose(); + for (final LayerSlice? slice in slices) { + slice?.dispose(); } } } @@ -442,8 +553,8 @@ abstract class LayerOperation { // layer operation. ui.Rect mapRect(ui.Rect contentRect); - void pre(SceneCanvas canvas, ui.Rect contentRect); - void post(SceneCanvas canvas, ui.Rect contentRect); + void pre(SceneCanvas canvas); + void post(SceneCanvas canvas); PlatformViewStyling createPlatformViewStyling(); @@ -453,11 +564,29 @@ abstract class LayerOperation { bool get shouldDrawIfEmpty; } -class PictureDrawCommand { - PictureDrawCommand(this.offset, this.picture); +sealed class LayerDrawCommand { +} + +class PictureDrawCommand extends LayerDrawCommand { + PictureDrawCommand(this.offset, this.picture, this.sliceIndex); - ui.Offset offset; - ui.Picture picture; + final int sliceIndex; + final ui.Offset offset; + final ScenePicture picture; +} + +class PlatformViewDrawCommand extends LayerDrawCommand { + PlatformViewDrawCommand(this.viewId, this.bounds, this.sliceIndex); + + final int sliceIndex; + final int viewId; + final ui.Rect bounds; +} + +class RetainedLayerDrawCommand extends LayerDrawCommand { + RetainedLayerDrawCommand(this.layer); + + final PictureEngineLayer layer; } // Represents how a platform view should be positioned in the scene. @@ -477,6 +606,17 @@ class PlatformViewPosition { bool get isZero => (offset == null) && (transform == null); + ui.Rect mapLocalToGlobal(ui.Rect rect) { + if (offset != null) { + return rect.shift(offset!); + } + if (transform != null) { + return transform!.transformRect(rect); + } + return rect; + } + + // Note that by construction only one of these can be set at any given time, not both. final ui.Offset? offset; final Matrix4? transform; @@ -527,6 +667,17 @@ class PlatformViewPosition { int get hashCode { return Object.hash(offset, transform); } + + @override + String toString() { + if (offset != null) { + return 'PlatformViewPosition(offset: $offset)'; + } + if (transform != null) { + return 'PlatformViewPosition(transform: $transform)'; + } + return 'PlatformViewPosition(zero)'; + } } // Represents the styling to be performed on a platform view when it is @@ -545,6 +696,10 @@ class PlatformViewStyling { final double opacity; final PlatformViewClip clip; + ui.Rect mapLocalToGlobal(ui.Rect rect) { + return position.mapLocalToGlobal(rect).intersect(clip.outerRect); + } + static PlatformViewStyling combine(PlatformViewStyling outer, PlatformViewStyling inner) { // Attempt to reuse one of the existing immutable objects. if (outer.isDefault) { @@ -575,6 +730,11 @@ class PlatformViewStyling { int get hashCode { return Object.hash(position, opacity, clip); } + + @override + String toString() { + return 'PlatformViewStyling(position: $position, clip: $clip, opacity: $opacity)'; + } } sealed class PlatformViewClip { @@ -642,7 +802,7 @@ class PlatformViewNoClip implements PlatformViewClip { ui.Rect get innerRect => ui.Rect.zero; @override - ui.Rect get outerRect => ui.Rect.zero; + ui.Rect get outerRect => ui.Rect.largest; } class PlatformViewRectClip implements PlatformViewClip { @@ -763,164 +923,137 @@ class PlatformViewPathClip implements PlatformViewClip { ui.Rect get outerRect => path.getBounds(); } +class LayerSliceBuilder { + factory LayerSliceBuilder() { + final (recorder, canvas) = debugRecorderFactory != null ? debugRecorderFactory!() : defaultRecorderFactory(); + return LayerSliceBuilder._(recorder, canvas); + } + LayerSliceBuilder._(this.recorder, this.canvas); + + @visibleForTesting + static (ui.PictureRecorder, SceneCanvas) Function()? debugRecorderFactory; + + static (ui.PictureRecorder, SceneCanvas) defaultRecorderFactory() { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final SceneCanvas canvas = ui.Canvas(recorder, ui.Rect.largest) as SceneCanvas; + return (recorder, canvas); + } + + final ui.PictureRecorder recorder; + final SceneCanvas canvas; + final List platformViews = []; +} + class LayerBuilder { factory LayerBuilder.rootLayer() { - return LayerBuilder._(null, EngineRootLayer(), null); + return LayerBuilder._(null, EngineRootLayer()); } factory LayerBuilder.childLayer({ required LayerBuilder parent, required PictureEngineLayer layer, - required LayerOperation operation }) { - return LayerBuilder._(parent, layer, operation); + return LayerBuilder._(parent, layer); } LayerBuilder._( this.parent, - this.layer, - this.operation); - - @visibleForTesting - static (ui.PictureRecorder, SceneCanvas) Function(ui.Rect)? debugRecorderFactory; + this.layer); final LayerBuilder? parent; final PictureEngineLayer layer; - final LayerOperation? operation; - final List pendingPictures = []; - List pendingPlatformViews = []; - ui.Rect? picturesRect; - ui.Rect? platformViewRect; - PlatformViewStyling? _memoizedPlatformViewStyling; + final List sliceBuilders = []; + final List drawCommands = []; + PlatformViewStyling? _memoizedPlatformViewStyling; PlatformViewStyling get platformViewStyling { - return _memoizedPlatformViewStyling ??= operation?.createPlatformViewStyling() ?? const PlatformViewStyling(); + return _memoizedPlatformViewStyling ??= layer.operation.createPlatformViewStyling(); } - (ui.PictureRecorder, SceneCanvas) _createRecorder(ui.Rect rect) { - if (debugRecorderFactory != null) { - return debugRecorderFactory!(rect); + PlatformViewStyling? _memoizedGlobalPlatformViewStyling; + PlatformViewStyling get globalPlatformViewStyling { + if (_memoizedGlobalPlatformViewStyling != null) { + return _memoizedGlobalPlatformViewStyling!; } - final ui.PictureRecorder recorder = ui.PictureRecorder(); - final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; - return (recorder, canvas); + if (parent != null) { + return _memoizedGlobalPlatformViewStyling ??= PlatformViewStyling.combine(parent!.globalPlatformViewStyling, platformViewStyling); + } + return _memoizedGlobalPlatformViewStyling ??= platformViewStyling; } - void flushSlices() { - if (pendingPictures.isNotEmpty || (operation?.shouldDrawIfEmpty ?? false)) { - // Merge the existing draw commands into a single picture and add a slice - // with that picture to the slice list. - final ui.Rect drawnRect = picturesRect ?? ui.Rect.zero; - final ui.Rect rect = operation?.mapRect(drawnRect) ?? drawnRect; - final (ui.PictureRecorder recorder, SceneCanvas canvas) = _createRecorder(rect); - - operation?.pre(canvas, rect); - for (final PictureDrawCommand command in pendingPictures) { - if (command.offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(command.offset.dx, command.offset.dy); - canvas.drawPicture(command.picture); - canvas.restore(); - } else { - canvas.drawPicture(command.picture); - } - } - operation?.post(canvas, rect); - final ui.Picture picture = recorder.endRecording(); - layer.slices.add(PictureSlice(picture as ScenePicture)); + LayerSliceBuilder getOrCreateSliceBuilderAtIndex(int index) { + while (sliceBuilders.length <= index) { + sliceBuilders.add(null); } - - if (pendingPlatformViews.isNotEmpty) { - // Take any pending platform views and lower them into a platform view - // slice. - ui.Rect? occlusionRect = platformViewRect; - if (occlusionRect != null && operation != null) { - occlusionRect = operation!.mapRect(occlusionRect); - } - layer.slices.add(PlatformViewSlice(pendingPlatformViews, occlusionRect)); + final LayerSliceBuilder? existingSliceBuilder = sliceBuilders[index]; + if (existingSliceBuilder != null) { + return existingSliceBuilder; } - - pendingPictures.clear(); - pendingPlatformViews = []; - - // All the pictures and platform views have been lowered into slices. Clear - // our occlusion rectangles. - picturesRect = null; - platformViewRect = null; + final LayerSliceBuilder newSliceBuilder = LayerSliceBuilder(); + layer.operation.pre(newSliceBuilder.canvas); + sliceBuilders[index] = newSliceBuilder; + return newSliceBuilder; } void addPicture( ui.Offset offset, ui.Picture picture, { - bool isComplexHint = false, - bool willChangeHint = false + required int sliceIndex, }) { - final ui.Rect cullRect = (picture as ScenePicture).cullRect; - final ui.Rect shiftedRect = cullRect.shift(offset); - - final ui.Rect? currentPlatformViewRect = platformViewRect; - if (currentPlatformViewRect != null) { - // Whenever we add a picture to our layer, we try to see if the picture - // will overlap with any platform views that are currently on top of our - // drawing surface. If they don't overlap with the platform views, they - // can be grouped with the existing pending pictures. - if (pendingPictures.isEmpty || currentPlatformViewRect.overlaps(shiftedRect)) { - // If they do overlap with the platform views, however, we should flush - // all the current content into slices and start anew with a fresh - // group of pictures and platform views that will be rendered on top of - // the previous content. Note that we also flush if we have no pending - // pictures to group with. This is the case when platform views are - // the first thing in our stack of objects to composite, and it doesn't - // make sense to try to put a picture slice below the first platform - // view slice, even if the picture doesn't overlap. - flushSlices(); - } + final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); + final SceneCanvas canvas = sliceBuilder.canvas; + if (offset != ui.Offset.zero) { + canvas.save(); + canvas.translate(offset.dx, offset.dy); + canvas.drawPicture(picture); + canvas.restore(); + } else { + canvas.drawPicture(picture); } - pendingPictures.add(PictureDrawCommand(offset, picture)); - picturesRect = picturesRect?.expandToInclude(shiftedRect) ?? shiftedRect; + drawCommands.add(PictureDrawCommand(offset, picture as ScenePicture, sliceIndex)); } void addPlatformView( int viewId, { - ui.Offset offset = ui.Offset.zero, - double width = 0.0, - double height = 0.0 + required ui.Rect bounds, + required int sliceIndex, }) { - final ui.Rect bounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); - platformViewRect = platformViewRect?.expandToInclude(bounds) ?? bounds; - pendingPlatformViews.add(PlatformView(viewId, bounds, platformViewStyling)); + final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); + sliceBuilder.platformViews.add(PlatformView(viewId, bounds, platformViewStyling)); + drawCommands.add(PlatformViewDrawCommand(viewId, bounds, sliceIndex)); } void mergeLayer(PictureEngineLayer layer) { - // When we merge layers, we attempt to merge slices as much as possible as - // well, based on ordering of pictures and platform views and reusing the - // occlusion logic for determining where we can lower each picture. - for (final LayerSlice slice in layer.slices) { - switch (slice) { - case PictureSlice(): - addPicture(ui.Offset.zero, slice.picture); - case PlatformViewSlice(): - final ui.Rect? occlusionRect = slice.occlusionRect; - if (occlusionRect != null) { - platformViewRect = platformViewRect?.expandToInclude(occlusionRect) ?? occlusionRect; - } - for (final PlatformView view in slice.views) { - // Merge the platform view styling of this layer with the nested - // platform views. - final PlatformViewStyling styling = PlatformViewStyling.combine( - platformViewStyling, - view.styling, - ); - pendingPlatformViews.add(PlatformView(view.viewId, view.bounds, styling)); - } + for (int i = 0; i < layer.slices.length; i++) { + final LayerSlice? slice = layer.slices[i]; + if (slice != null) { + final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(i); + sliceBuilder.canvas.drawPicture(slice.picture); + sliceBuilder.platformViews.addAll(slice.platformViews.map((PlatformView view) { + return PlatformView(view.viewId, view.bounds, PlatformViewStyling.combine(platformViewStyling, view.styling)); + })); } } + drawCommands.add(RetainedLayerDrawCommand(layer)); } - PictureEngineLayer build() { - // Lower any pending pictures or platform views to their respective slices. - flushSlices(); + PictureEngineLayer sliceUp() { + final List slices = sliceBuilders.map((LayerSliceBuilder? builder) { + if (builder == null) { + return null; + } + layer.operation.post(builder.canvas); + final ScenePicture picture = builder.recorder.endRecording() as ScenePicture; + return LayerSlice(picture, builder.platformViews); + }).toList(); + layer.slices = slices; return layer; } + + PictureEngineLayer build() { + layer.drawCommands = drawCommands; + layer.platformViewStyling = platformViewStyling; + return sliceUp(); + } } diff --git a/lib/web_ui/lib/src/engine/scene_builder.dart b/lib/web_ui/lib/src/engine/scene_builder.dart index af84cb00990c6..6d6db06e51822 100644 --- a/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/scene_builder.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' as math; import 'dart:typed_data'; import 'package:ui/src/engine.dart'; @@ -62,17 +63,115 @@ class EngineScene implements ui.Scene { final ui.Rect canvasRect = ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()); final ui.Canvas canvas = ui.Canvas(recorder, canvasRect); - // Only rasterizes the picture slices. - for (final PictureSlice slice in rootLayer.slices.whereType()) { - canvas.drawPicture(slice.picture); + // Only rasterizes the pictures. + for (final LayerSlice? slice in rootLayer.slices) { + if (slice != null) { + canvas.drawPicture(slice.picture); + } } return recorder.endRecording().toImageSync(width, height); } } +sealed class OcclusionMapNode { + bool overlaps(ui.Rect rect); + OcclusionMapNode insert(ui.Rect rect); + ui.Rect get boundingBox; +} + +class OcclusionMapEmpty implements OcclusionMapNode { + @override + ui.Rect get boundingBox => ui.Rect.zero; + + @override + OcclusionMapNode insert(ui.Rect rect) => OcclusionMapLeaf(rect); + + @override + bool overlaps(ui.Rect rect) => false; + +} + +class OcclusionMapLeaf implements OcclusionMapNode { + OcclusionMapLeaf(this.rect); + + final ui.Rect rect; + + @override + ui.Rect get boundingBox => rect; + + @override + OcclusionMapNode insert(ui.Rect other) => OcclusionMapBranch(this, OcclusionMapLeaf(other)); + + @override + bool overlaps(ui.Rect other) => rect.overlaps(other); +} + +class OcclusionMapBranch implements OcclusionMapNode { + OcclusionMapBranch(this.left, this.right) + : boundingBox = left.boundingBox.expandToInclude(right.boundingBox); + + final OcclusionMapNode left; + final OcclusionMapNode right; + + @override + final ui.Rect boundingBox; + + double _areaOfUnion(ui.Rect first, ui.Rect second) { + return (math.max(first.right, second.right) - math.min(first.left, second.left)) + * (math.max(first.bottom, second.bottom) - math.max(first.top, second.top)); + } + + @override + OcclusionMapNode insert(ui.Rect other) { + // Try to create nodes with the smallest possible area + final double leftOtherArea = _areaOfUnion(left.boundingBox, other); + final double rightOtherArea = _areaOfUnion(right.boundingBox, other); + final double leftRightArea = boundingBox.width * boundingBox.height; + if (leftOtherArea < rightOtherArea) { + if (leftOtherArea < leftRightArea) { + return OcclusionMapBranch( + left.insert(other), + right, + ); + } + } else { + if (rightOtherArea < leftRightArea) { + return OcclusionMapBranch( + left, + right.insert(other), + ); + } + } + return OcclusionMapBranch(this, OcclusionMapLeaf(other)); + } + + @override + bool overlaps(ui.Rect rect) { + if (!boundingBox.overlaps(rect)) { + return false; + } + return left.overlaps(rect) || right.overlaps(rect); + } +} + +class OcclusionMap { + OcclusionMapNode root = OcclusionMapEmpty(); + + void addRect(ui.Rect rect) => root = root.insert(rect); + + bool overlaps(ui.Rect rect) => root.overlaps(rect); +} + +class SceneSlice { + final OcclusionMap pictureOcclusionMap = OcclusionMap(); + final OcclusionMap platformViewOcclusionMap = OcclusionMap(); +} + class EngineSceneBuilder implements ui.SceneBuilder { LayerBuilder currentBuilder = LayerBuilder.rootLayer(); + final List sceneSlices = [SceneSlice()]; + @override void addPerformanceOverlay(int enabledOptions, ui.Rect bounds) { // We don't plan to implement this on the web. @@ -86,15 +185,44 @@ class EngineSceneBuilder implements ui.SceneBuilder { bool isComplexHint = false, bool willChangeHint = false }) { + final int sliceIndex = _placePicture(offset, picture as ScenePicture); currentBuilder.addPicture( offset, picture, - isComplexHint: - isComplexHint, - willChangeHint: willChangeHint + sliceIndex: sliceIndex, ); } + // This function determines the lowest scene slice that this picture can be placed + // into and adds it to that slice's occlusion map. + // + // The picture is placed in the last slice where it either intersects with a picture + // in the slice or it intersects with a platform view in the preceding slice. If the + // picture intersects with a platform view in the last slice, a new slice is added at + // the end and the picture goes in there. + int _placePicture(ui.Offset offset, ScenePicture picture) { + final ui.Rect cullRect = picture.cullRect.shift(offset); + final ui.Rect mappedCullRect = currentBuilder.globalPlatformViewStyling.mapLocalToGlobal(cullRect); + int sliceIndex = sceneSlices.length; + while (sliceIndex > 0) { + final SceneSlice sliceBelow = sceneSlices[sliceIndex - 1]; + if (sliceBelow.platformViewOcclusionMap.overlaps(mappedCullRect)) { + break; + } + sliceIndex--; + if (sliceBelow.pictureOcclusionMap.overlaps(mappedCullRect)) { + break; + } + } + if (sliceIndex == sceneSlices.length) { + // Insert a new slice. + sceneSlices.add(SceneSlice()); + } + final SceneSlice slice = sceneSlices[sliceIndex]; + slice.pictureOcclusionMap.addRect(mappedCullRect); + return sliceIndex; + } + @override void addPlatformView( int viewId, { @@ -102,17 +230,94 @@ class EngineSceneBuilder implements ui.SceneBuilder { double width = 0.0, double height = 0.0 }) { + final ui.Rect platformViewRect = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); + final int sliceIndex = _placePlatformView(viewId, platformViewRect); currentBuilder.addPlatformView( viewId, - offset: offset, - width: width, - height: height + bounds: platformViewRect, + sliceIndex: sliceIndex, ); } + // This function determines the lowest scene slice this platform view can be placed + // into and adds it to that slice's occlusion map. + // + // The platform view is placed into the last slice where it intersects with a picture + // or a platform view. + int _placePlatformView( + int viewId, + ui.Rect rect, { + PlatformViewStyling styling = const PlatformViewStyling(), + }) { + final PlatformViewStyling combinedStyling = PlatformViewStyling.combine(currentBuilder.globalPlatformViewStyling, styling); + final ui.Rect globalPlatformViewRect = combinedStyling.mapLocalToGlobal(rect); + int sliceIndex = sceneSlices.length - 1; + while (sliceIndex > 0) { + final SceneSlice slice = sceneSlices[sliceIndex]; + if (slice.platformViewOcclusionMap.overlaps(globalPlatformViewRect) || + slice.pictureOcclusionMap.overlaps(globalPlatformViewRect)) { + break; + } + sliceIndex--; + } + final SceneSlice slice = sceneSlices[sliceIndex]; + slice.platformViewOcclusionMap.addRect(globalPlatformViewRect); + return sliceIndex; + } + @override void addRetained(ui.EngineLayer retainedLayer) { - currentBuilder.mergeLayer(retainedLayer as PictureEngineLayer); + final PictureEngineLayer placedEngineLayer = _placeRetainedLayer(retainedLayer as PictureEngineLayer); + currentBuilder.mergeLayer(placedEngineLayer); + } + + PictureEngineLayer _placeRetainedLayer(PictureEngineLayer retainedLayer) { + bool needsRebuild = false; + final List revisedDrawCommands = []; + for (final LayerDrawCommand command in retainedLayer.drawCommands) { + switch (command) { + case PictureDrawCommand(offset: final ui.Offset offset, picture: final ScenePicture picture): + final int sliceIndex = _placePicture(offset, picture); + if (command.sliceIndex != sliceIndex) { + needsRebuild = true; + } + revisedDrawCommands.add(PictureDrawCommand(offset, picture, sliceIndex)); + case PlatformViewDrawCommand(viewId: final int viewId, bounds: final ui.Rect bounds): + final int sliceIndex = _placePlatformView(viewId, bounds); + if (command.sliceIndex != sliceIndex) { + needsRebuild = true; + } + revisedDrawCommands.add(PlatformViewDrawCommand(viewId, bounds, sliceIndex)); + case RetainedLayerDrawCommand(layer: final PictureEngineLayer sublayer): + final PictureEngineLayer revisedSublayer = _placeRetainedLayer(sublayer); + if (sublayer != revisedSublayer) { + needsRebuild = true; + } + revisedDrawCommands.add(RetainedLayerDrawCommand(revisedSublayer)); + } + } + + if (!needsRebuild) { + // No elements changed which slice position they are in, so we can simply + // merge the existing layer down and don't have to redraw individual elements. + return retainedLayer; + } + + // Otherwise, we replace the commands of the layer to create a new one. + currentBuilder = LayerBuilder.childLayer(parent: currentBuilder, layer: retainedLayer.emptyClone()); + for (final LayerDrawCommand command in revisedDrawCommands) { + switch (command) { + case PictureDrawCommand(offset: final ui.Offset offset, picture: final ScenePicture picture): + currentBuilder.addPicture(offset, picture, sliceIndex: command.sliceIndex); + case PlatformViewDrawCommand(viewId: final int viewId, bounds: final ui.Rect bounds): + currentBuilder.addPlatformView(viewId, bounds: bounds, sliceIndex: command.sliceIndex); + case RetainedLayerDrawCommand(layer: final PictureEngineLayer layer): + currentBuilder.mergeLayer(layer); + } + } + final PictureEngineLayer newLayer = currentBuilder.build(); + currentBuilder = currentBuilder.parent!; + return newLayer; } @override @@ -132,30 +337,21 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.ImageFilter filter, { ui.BlendMode blendMode = ui.BlendMode.srcOver, ui.BackdropFilterEngineLayer? oldLayer - }) => pushLayer( - BackdropFilterLayer(), - BackdropFilterOperation(filter, blendMode), - ); + }) => pushLayer(BackdropFilterLayer(BackdropFilterOperation(filter, blendMode))); @override ui.ClipPathEngineLayer pushClipPath( ui.Path path, { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.ClipPathEngineLayer? oldLayer - }) => pushLayer( - ClipPathLayer(), - ClipPathOperation(path as ScenePath, clipBehavior), - ); + }) => pushLayer(ClipPathLayer(ClipPathOperation(path as ScenePath, clipBehavior))); @override ui.ClipRRectEngineLayer pushClipRRect( ui.RRect rrect, { required ui.Clip clipBehavior, ui.ClipRRectEngineLayer? oldLayer - }) => pushLayer( - ClipRRectLayer(), - ClipRRectOperation(rrect, clipBehavior) - ); + }) => pushLayer(ClipRRectLayer(ClipRRectOperation(rrect, clipBehavior))); @override ui.ClipRectEngineLayer pushClipRect( @@ -163,20 +359,14 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.ClipRectEngineLayer? oldLayer }) { - return pushLayer( - ClipRectLayer(), - ClipRectOperation(rect, clipBehavior) - ); + return pushLayer(ClipRectLayer(ClipRectOperation(rect, clipBehavior))); } @override ui.ColorFilterEngineLayer pushColorFilter( ui.ColorFilter filter, { ui.ColorFilterEngineLayer? oldLayer - }) => pushLayer( - ColorFilterLayer(), - ColorFilterOperation(filter), - ); + }) => pushLayer(ColorFilterLayer(ColorFilterOperation(filter))); @override ui.ImageFilterEngineLayer pushImageFilter( @@ -184,8 +374,7 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.Offset offset = ui.Offset.zero, ui.ImageFilterEngineLayer? oldLayer }) => pushLayer( - ImageFilterLayer(), - ImageFilterOperation(filter as SceneImageFilter, offset), + ImageFilterLayer(ImageFilterOperation(filter as SceneImageFilter, offset)), ); @override @@ -193,19 +382,14 @@ class EngineSceneBuilder implements ui.SceneBuilder { double dx, double dy, { ui.OffsetEngineLayer? oldLayer - }) => pushLayer( - OffsetLayer(), - OffsetOperation(dx, dy) - ); + }) => pushLayer(OffsetLayer(OffsetOperation(dx, dy))); @override ui.OpacityEngineLayer pushOpacity(int alpha, { ui.Offset offset = ui.Offset.zero, ui.OpacityEngineLayer? oldLayer - }) => pushLayer( - OpacityLayer(), - OpacityOperation(alpha, offset), - ); + }) => pushLayer(OpacityLayer(OpacityOperation(alpha, offset))); + @override ui.ShaderMaskEngineLayer pushShaderMask( ui.Shader shader, @@ -214,18 +398,14 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.ShaderMaskEngineLayer? oldLayer, ui.FilterQuality filterQuality = ui.FilterQuality.low }) => pushLayer( - ShaderMaskLayer(), - ShaderMaskOperation(shader, maskRect, blendMode) + ShaderMaskLayer(ShaderMaskOperation(shader, maskRect, blendMode)), ); @override ui.TransformEngineLayer pushTransform( Float64List matrix4, { ui.TransformEngineLayer? oldLayer - }) => pushLayer( - TransformLayer(), - TransformOperation(matrix4), - ); + }) => pushLayer(TransformLayer(TransformOperation(matrix4))); @override void setProperties( @@ -260,11 +440,10 @@ class EngineSceneBuilder implements ui.SceneBuilder { currentBuilder.mergeLayer(layer); } - T pushLayer(T layer, LayerOperation operation) { + T pushLayer(T layer) { currentBuilder = LayerBuilder.childLayer( parent: currentBuilder, layer: layer, - operation: operation ); return layer; } diff --git a/lib/web_ui/lib/src/engine/scene_painting.dart b/lib/web_ui/lib/src/engine/scene_painting.dart index 1ef39d24480cd..4f70633328c67 100644 --- a/lib/web_ui/lib/src/engine/scene_painting.dart +++ b/lib/web_ui/lib/src/engine/scene_painting.dart @@ -4,6 +4,8 @@ import 'package:ui/ui.dart' as ui; +import 'vector_math.dart'; + // These are additional APIs that are not part of the `dart:ui` interface that // are needed internally to properly implement a `SceneBuilder` on top of the // generic Canvas/Picture api. @@ -22,6 +24,10 @@ abstract class SceneImageFilter implements ui.ImageFilter { // gives the maximum draw boundary for a picture with the given input bounds after it // has been processed by the filter. ui.Rect filterBounds(ui.Rect inputBounds); + + // The matrix image filter changes the position of the content, so when positioning + // platform views and calculating occlusion we need to take its transform into account. + Matrix4? get transform; } abstract class ScenePath implements ui.Path { diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index 53504204a4bcb..909c3860419b5 100644 --- a/lib/web_ui/lib/src/engine/scene_view.dart +++ b/lib/web_ui/lib/src/engine/scene_view.dart @@ -95,23 +95,24 @@ class EngineSceneView { flutterView.physicalSize.width, flutterView.physicalSize.height, ); - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; final List picturesToRender = []; final List originalPicturesToRender = []; - for (final LayerSlice slice in slices) { - if (slice is PictureSlice) { - final ui.Rect clippedRect = slice.picture.cullRect.intersect(screenBounds); - if (clippedRect.isEmpty) { - // This picture is completely offscreen, so don't render it at all - continue; - } else if (clippedRect == slice.picture.cullRect) { - // The picture doesn't need to be clipped, just render the original - originalPicturesToRender.add(slice.picture); - picturesToRender.add(slice.picture); - } else { - originalPicturesToRender.add(slice.picture); - picturesToRender.add(pictureRenderer.clipPicture(slice.picture, clippedRect)); - } + for (final LayerSlice? slice in slices) { + if (slice == null) { + continue; + } + final ui.Rect clippedRect = slice.picture.cullRect.intersect(screenBounds); + if (clippedRect.isEmpty) { + // This picture is completely offscreen, so don't render it at all + continue; + } else if (clippedRect == slice.picture.cullRect) { + // The picture doesn't need to be clipped, just render the original + originalPicturesToRender.add(slice.picture); + picturesToRender.add(slice.picture); + } else { + originalPicturesToRender.add(slice.picture); + picturesToRender.add(pictureRenderer.clipPicture(slice.picture, clippedRect)); } } final Map renderMap; @@ -132,58 +133,55 @@ class EngineSceneView { final List reusableContainers = List.from(containers); final List newContainers = []; - for (final LayerSlice slice in slices) { - switch (slice) { - case PictureSlice(): - final DomImageBitmap? bitmap = renderMap[slice.picture]; - if (bitmap == null) { - // We didn't render this slice because no part of it is visible. - continue; - } - PictureSliceContainer? container; - for (int j = 0; j < reusableContainers.length; j++) { - final SliceContainer? candidate = reusableContainers[j]; - if (candidate is PictureSliceContainer) { - container = candidate; - reusableContainers[j] = null; - break; - } + for (final LayerSlice? slice in slices) { + if (slice == null) { + continue; + } + final DomImageBitmap? bitmap = renderMap[slice.picture]; + if (bitmap != null) { + PictureSliceContainer? container; + for (int j = 0; j < reusableContainers.length; j++) { + final SliceContainer? candidate = reusableContainers[j]; + if (candidate is PictureSliceContainer) { + container = candidate; + reusableContainers[j] = null; + break; } + } - final ui.Rect clippedBounds = slice.picture.cullRect.intersect(screenBounds); - if (container != null) { - container.bounds = clippedBounds; - } else { - container = PictureSliceContainer(clippedBounds); - } - container.updateContents(); - container.renderBitmap(bitmap); - newContainers.add(container); - - case PlatformViewSlice(): - for (final PlatformView view in slice.views) { - // TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`, - // instead of using `EnginePlatformDispatcher...implicitView` directly, - // or make the FlutterView "register" like in canvaskit. - // Ensure the platform view contents are injected in the DOM. - EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId); - - // Attempt to reuse a container for the existing view - PlatformViewContainer? container; - for (int j = 0; j < reusableContainers.length; j++) { - final SliceContainer? candidate = reusableContainers[j]; - if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) { - container = candidate; - reusableContainers[j] = null; - break; - } - } - container ??= PlatformViewContainer(view.viewId); - container.bounds = view.bounds; - container.styling = view.styling; - container.updateContents(); - newContainers.add(container); + final ui.Rect clippedBounds = slice.picture.cullRect.intersect(screenBounds); + if (container != null) { + container.bounds = clippedBounds; + } else { + container = PictureSliceContainer(clippedBounds); + } + container.updateContents(); + container.renderBitmap(bitmap); + newContainers.add(container); + } + + for (final PlatformView view in slice.platformViews) { + // TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`, + // instead of using `EnginePlatformDispatcher...implicitView` directly, + // or make the FlutterView "register" like in canvaskit. + // Ensure the platform view contents are injected in the DOM. + EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId); + + // Attempt to reuse a container for the existing view + PlatformViewContainer? container; + for (int j = 0; j < reusableContainers.length; j++) { + final SliceContainer? candidate = reusableContainers[j]; + if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) { + container = candidate; + reusableContainers[j] = null; + break; } + } + container ??= PlatformViewContainer(view.viewId); + container.bounds = view.bounds; + container.styling = view.styling; + container.updateContents(); + newContainers.add(container); } } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart index 7c611d110baef..06b8c47cc34d9 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart @@ -88,6 +88,9 @@ class SkwasmBlurFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.blur($sigmaX, $sigmaY, ${tileModeString(tileMode)})'; + + @override + Matrix4? get transform => null; } class SkwasmDilateFilter extends SkwasmImageFilter { @@ -105,6 +108,9 @@ class SkwasmDilateFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.dilate($radiusX, $radiusY)'; + + @override + Matrix4? get transform => null; } class SkwasmErodeFilter extends SkwasmImageFilter { @@ -122,6 +128,9 @@ class SkwasmErodeFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.erode($radiusX, $radiusY)'; + + @override + Matrix4? get transform => null; } class SkwasmMatrixFilter extends SkwasmImageFilter { @@ -144,6 +153,9 @@ class SkwasmMatrixFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.matrix($matrix4, $filterQuality)'; + + @override + Matrix4? get transform => Matrix4.fromFloat32List(toMatrix32(matrix4)); } class SkwasmColorImageFilter extends SkwasmImageFilter { @@ -162,6 +174,9 @@ class SkwasmColorImageFilter extends SkwasmImageFilter { @override String toString() => filter.toString(); + + @override + Matrix4? get transform => null; } class SkwasmComposedImageFilter extends SkwasmImageFilter { @@ -183,6 +198,16 @@ class SkwasmComposedImageFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.compose($outer, $inner)'; + + @override + Matrix4? get transform { + final outerTransform = outer.transform; + final innerTransform = inner.transform; + if (outerTransform != null && innerTransform != null) { + return outerTransform.multiplied(innerTransform); + } + return outerTransform ?? innerTransform; + } } typedef ColorFilterHandleBorrow = void Function(ColorFilterHandle handle); diff --git a/lib/web_ui/test/engine/scene_builder_test.dart b/lib/web_ui/test/engine/scene_builder_test.dart index d8826e8da6033..a94a65159b4ef 100644 --- a/lib/web_ui/test/engine/scene_builder_test.dart +++ b/lib/web_ui/test/engine/scene_builder_test.dart @@ -16,7 +16,7 @@ void main() { void testMain() { setUpAll(() { - LayerBuilder.debugRecorderFactory = (ui.Rect rect) { + LayerSliceBuilder.debugRecorderFactory = () { final StubSceneCanvas canvas = StubSceneCanvas(); final StubPictureRecorder recorder = StubPictureRecorder(canvas); return (recorder, canvas); @@ -24,7 +24,7 @@ void testMain() { }); tearDownAll(() { - LayerBuilder.debugRecorderFactory = null; + LayerSliceBuilder.debugRecorderFactory = null; }); group('EngineSceneBuilder', () { @@ -35,23 +35,23 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; expect(slices.length, 1); - expect(slices[0], pictureSliceWithRect(pictureRect)); + expect(slices[0], layerSlice(withPictureRect: pictureRect)); }); test('two pictures', () { final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 400, 400, 400); + const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 300, 400, 400); sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; expect(slices.length, 1); - expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(100, 100, 400, 400))); + expect(slices[0], layerSlice(withPictureRect: const ui.Rect.fromLTRB(100, 100, 400, 400))); }); test('picture + platform view (overlapping)', () { @@ -68,10 +68,11 @@ void testMain() { ); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 2); - expect(slices[0], pictureSliceWithRect(pictureRect)); - expect(slices[1], platformViewSliceWithViews([ + final List slices = scene.rootLayer.slices; + expect(slices.length, 1); + expect(slices[0], layerSlice( + withPictureRect: pictureRect, + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); }); @@ -90,12 +91,12 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; expect(slices.length, 2); - expect(slices[0], platformViewSliceWithViews([ + expect(slices[0], layerSlice(withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); - expect(slices[1], pictureSliceWithRect(pictureRect)); + expect(slices[1], layerSlice(withPictureRect: pictureRect)); }); test('platform view sandwich (overlapping)', () { @@ -114,13 +115,14 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 3); - expect(slices[0], pictureSliceWithRect(pictureRect1)); - expect(slices[1], platformViewSliceWithViews([ + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], layerSlice( + withPictureRect: pictureRect1, + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); - expect(slices[2], pictureSliceWithRect(pictureRect2)); + expect(slices[1], layerSlice(withPictureRect: pictureRect2)); }); test('platform view sandwich (non-overlapping)', () { @@ -139,14 +141,15 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; // The top picture does not overlap with the platform view, so it should // be grouped into the slice below it to reduce the number of canvases we // need. - expect(slices.length, 2); - expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(50, 50, 200, 200))); - expect(slices[1], platformViewSliceWithViews([ + expect(slices.length, 1); + expect(slices[0], layerSlice( + withPictureRect: const ui.Rect.fromLTRB(50, 50, 200, 200), + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); }); @@ -169,34 +172,99 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(const ui.Rect.fromLTRB(0, 0, 100, 100))); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 3); - expect(slices[0], pictureSliceWithRect(pictureRect1)); - expect(slices[1], platformViewSliceWithViews([ + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], layerSlice( + withPictureRect: pictureRect1, + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling(position: PlatformViewPosition.offset(ui.Offset(150, 150)))) ])); - expect(slices[2], pictureSliceWithRect(const ui.Rect.fromLTRB(200, 200, 300, 300))); + expect(slices[1], layerSlice(withPictureRect: const ui.Rect.fromLTRB(200, 200, 300, 300))); + }); + + test('grid view test', () { + // This test case covers a grid of elements, where each element is a platform + // view that has flutter content underneath it and on top of it. + // See a detailed explanation of this use-case in the following flutter issue: + // https://github.com/flutter/flutter/issues/149863 + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const double padding = 10; + const double tileSize = 50; + final List expectedPlatformViews = []; + for (int x = 0; x < 10; x++) { + for (int y = 0; y < 10; y++) { + final ui.Offset offset = ui.Offset( + padding + (tileSize + padding) * x, + padding + (tileSize + padding) * y, + ); + sceneBuilder.pushOffset(offset.dx, offset.dy); + sceneBuilder.addPicture( + ui.Offset.zero, + StubPicture(const ui.Rect.fromLTWH(0, 0, tileSize, tileSize)) + ); + sceneBuilder.addPlatformView( + 1, + offset: const ui.Offset(5, 5), + width: tileSize - 10, + height: tileSize - 10, + ); + sceneBuilder.addPicture( + const ui.Offset(10, 10), + StubPicture(const ui.Rect.fromLTWH(0, 0, tileSize - 20, tileSize - 20)), + ); + sceneBuilder.pop(); + expectedPlatformViews.add(PlatformView( + 1, + const ui.Rect.fromLTRB(5.0, 5.0, tileSize - 5.0, tileSize - 5.0), + PlatformViewStyling(position: PlatformViewPosition.offset(offset)) + )); + } + } + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + + // It is important that the optimizations of the scene builder result in + // there only being two scene slices. + expect(slices.length, 2); + expect(slices[0], layerSlice( + withPictureRect: const ui.Rect.fromLTRB( + padding, + padding, + 10 * (padding + tileSize), + 10 * (padding + tileSize) + ), + withPlatformViews: expectedPlatformViews, + )); + expect(slices[1], layerSlice(withPictureRect: const ui.Rect.fromLTRB( + padding + 10, + padding + 10, + 10 * (padding + tileSize) - 10, + 10 * (padding + tileSize) - 10, + ))); }); }); } -PictureSliceMatcher pictureSliceWithRect(ui.Rect rect) => PictureSliceMatcher(rect); -PlatformViewSliceMatcher platformViewSliceWithViews(List views) - => PlatformViewSliceMatcher(views); +LayerSliceMatcher layerSlice({ + ui.Rect withPictureRect = ui.Rect.zero, + List withPlatformViews = const [], +}) => LayerSliceMatcher(withPictureRect, withPlatformViews); +class LayerSliceMatcher extends Matcher { + LayerSliceMatcher(this.expectedPictureRect, this.expectedPlatformViews); -class PictureSliceMatcher extends Matcher { - PictureSliceMatcher(this.expectedRect); - - final ui.Rect expectedRect; + final ui.Rect expectedPictureRect; + final List expectedPlatformViews; @override Description describe(Description description) { - return description.add(''); + return description.add(''); } @override bool matches(dynamic item, Map matchState) { - if (item is! PictureSlice) { + if (item is! LayerSlice) { return false; } final ScenePicture picture = item.picture; @@ -204,50 +272,28 @@ class PictureSliceMatcher extends Matcher { return false; } - if (picture.cullRect != expectedRect) { + if (picture.cullRect != expectedPictureRect) { return false; } - return true; - } -} - -class PlatformViewSliceMatcher extends Matcher { - PlatformViewSliceMatcher(this.expectedPlatformViews); - - final List expectedPlatformViews; - - @override - Description describe(Description description) { - return description.add(''); - } - - @override - bool matches(dynamic item, Map matchState) { - if (item is! PlatformViewSlice) { + if (item.platformViews.length != expectedPlatformViews.length) { return false; } - if (item.views.length != expectedPlatformViews.length) { - return false; - } - - for (int i = 0; i < item.views.length; i++) { + for (int i = 0; i < item.platformViews.length; i++) { final PlatformView expectedView = expectedPlatformViews[i]; - final PlatformView actualView = item.views[i]; + final PlatformView actualView = item.platformViews[i]; if (expectedView.viewId != actualView.viewId) { - print('viewID mismatch'); return false; } if (expectedView.bounds != actualView.bounds) { - print('bounds mismatch'); return false; } if (expectedView.styling != actualView.styling) { - print('styling mismatch'); return false; } } + return true; } } diff --git a/lib/web_ui/test/engine/scene_builder_utils.dart b/lib/web_ui/test/engine/scene_builder_utils.dart index 033177c8d730b..ec014e70548ff 100644 --- a/lib/web_ui/test/engine/scene_builder_utils.dart +++ b/lib/web_ui/test/engine/scene_builder_utils.dart @@ -36,8 +36,12 @@ class StubPicture implements ScenePicture { class StubCompositePicture extends StubPicture { StubCompositePicture(this.children) : super( children.fold(null, (ui.Rect? previousValue, StubPicture child) { + final ui.Rect childRect = child.cullRect; + if (childRect.isEmpty) { + return previousValue; + } return previousValue?.expandToInclude(child.cullRect) ?? child.cullRect; - })! + }) ?? ui.Rect.zero ); final List children; diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 80093bb0313f9..58ff09de66cdb 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -172,7 +172,7 @@ void testMain() { 120, )); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PictureSlice(picture)); + rootLayer.slices.add(LayerSlice(picture, [])); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); @@ -205,7 +205,7 @@ void testMain() { const ui.Rect.fromLTWH(50, 80, 100, 120), const PlatformViewStyling()); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PlatformViewSlice([platformView], null)); + rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), [platformView])); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); @@ -246,7 +246,7 @@ void testMain() { )); pictures.add(picture); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PictureSlice(picture)); + rootLayer.slices.add(LayerSlice(picture, [])); final EngineScene scene = EngineScene(rootLayer); renderFutures.add(sceneView.renderScene(scene, null)); } @@ -267,7 +267,7 @@ void testMain() { )); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PictureSlice(picture)); + rootLayer.slices.add(LayerSlice(picture, [])); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); From 8ee53bc26167ebd7bd53333b466e230c62ecafa6 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Fri, 27 Sep 2024 09:45:29 -0700 Subject: [PATCH 2/5] Fix saveLayer bounds. --- lib/web_ui/lib/src/engine/layers.dart | 12 ++++++------ lib/web_ui/lib/src/engine/scene_builder.dart | 1 + lib/web_ui/test/ui/scene_builder_test.dart | 20 +++++++++++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/layers.dart b/lib/web_ui/lib/src/engine/layers.dart index 380194978bad8..bbd13b0c3d1c5 100644 --- a/lib/web_ui/lib/src/engine/layers.dart +++ b/lib/web_ui/lib/src/engine/layers.dart @@ -61,7 +61,7 @@ class BackdropFilterOperation implements LayerOperation { @override void pre(SceneCanvas canvas) { - canvas.saveLayerWithFilter(null, ui.Paint()..blendMode = mode, filter); + canvas.saveLayerWithFilter(ui.Rect.largest, ui.Paint()..blendMode = mode, filter); } @override @@ -103,7 +103,7 @@ class ClipPathOperation implements LayerOperation { canvas.save(); canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(null, ui.Paint()); + canvas.saveLayer(path.getBounds(), ui.Paint()); } } @@ -237,7 +237,7 @@ class ColorFilterOperation implements LayerOperation { @override void pre(SceneCanvas canvas) { - canvas.saveLayer(null, ui.Paint()..colorFilter = filter); + canvas.saveLayer(ui.Rect.largest, ui.Paint()..colorFilter = filter); } @override @@ -278,7 +278,7 @@ class ImageFilterOperation implements LayerOperation { canvas.save(); canvas.translate(offset.dx, offset.dy); } - canvas.saveLayer(null, ui.Paint()..imageFilter = filter); + canvas.saveLayer(ui.Rect.largest, ui.Paint()..imageFilter = filter); } @override @@ -380,7 +380,7 @@ class OpacityOperation implements LayerOperation { canvas.translate(offset.dx, offset.dy); } canvas.saveLayer( - null, + ui.Rect.largest, ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0) ); } @@ -469,7 +469,7 @@ class ShaderMaskOperation implements LayerOperation { @override void pre(SceneCanvas canvas) { canvas.saveLayer( - null, + maskRect, ui.Paint(), ); } diff --git a/lib/web_ui/lib/src/engine/scene_builder.dart b/lib/web_ui/lib/src/engine/scene_builder.dart index 6d6db06e51822..0b8bca704a574 100644 --- a/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/scene_builder.dart @@ -186,6 +186,7 @@ class EngineSceneBuilder implements ui.SceneBuilder { bool willChangeHint = false }) { final int sliceIndex = _placePicture(offset, picture as ScenePicture); + print('adding picture to sliceIndex: $sliceIndex'); currentBuilder.addPicture( offset, picture, diff --git a/lib/web_ui/test/ui/scene_builder_test.dart b/lib/web_ui/test/ui/scene_builder_test.dart index aeaef07ad0c5a..1ffda74df5264 100644 --- a/lib/web_ui/test/ui/scene_builder_test.dart +++ b/lib/web_ui/test/ui/scene_builder_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:math' as math; +import 'dart:math'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -243,7 +244,7 @@ Future testMain() async { await matchGoldenFile('scene_builder_empty_backdrop_filter_with_clip.png', region: region); }); - test('image filter layer', () async { + test('blur image filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushImageFilter(ui.ImageFilter.blur( sigmaX: 5.0, @@ -262,6 +263,23 @@ Future testMain() async { await matchGoldenFile('scene_builder_image_filter.png', region: region); }); + test('matrix image filter layer', () async { + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder.pushOffset(50.0, 50.0); + + final Matrix4 matrix = Matrix4.rotationZ(pi / 18); + final ui.ImageFilter matrixFilter = ui.ImageFilter.matrix(toMatrix64(matrix.storage)); + sceneBuilder.pushImageFilter(matrixFilter); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawRect( + region, + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); + })); + await renderScene(sceneBuilder.build()); + await matchGoldenFile('scene_builder_matrix_image_filter.png', region: region); + }); + // Regression test for https://github.com/flutter/flutter/issues/154303 test('image filter layer with offset', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); From 29859d480d50c406e254fec361fdffd51832e036 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Fri, 27 Sep 2024 10:03:15 -0700 Subject: [PATCH 3/5] Remove debug print. --- lib/web_ui/lib/src/engine/scene_builder.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/scene_builder.dart b/lib/web_ui/lib/src/engine/scene_builder.dart index 0b8bca704a574..6d6db06e51822 100644 --- a/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/scene_builder.dart @@ -186,7 +186,6 @@ class EngineSceneBuilder implements ui.SceneBuilder { bool willChangeHint = false }) { final int sliceIndex = _placePicture(offset, picture as ScenePicture); - print('adding picture to sliceIndex: $sliceIndex'); currentBuilder.addPicture( offset, picture, From 452984855f34aed6a0e78faab8e5adb8dcb5a424 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Fri, 27 Sep 2024 12:28:37 -0700 Subject: [PATCH 4/5] ShaderMask shouldn't constrain the savelayer rect. --- lib/web_ui/lib/src/engine/layers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/layers.dart b/lib/web_ui/lib/src/engine/layers.dart index bbd13b0c3d1c5..538b6d541ba34 100644 --- a/lib/web_ui/lib/src/engine/layers.dart +++ b/lib/web_ui/lib/src/engine/layers.dart @@ -469,7 +469,7 @@ class ShaderMaskOperation implements LayerOperation { @override void pre(SceneCanvas canvas) { canvas.saveLayer( - maskRect, + ui.Rect.largest, ui.Paint(), ); } From a52bd5566aafbd5957bcb1fc20a02da8aaf78cb4 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Fri, 27 Sep 2024 14:29:40 -0700 Subject: [PATCH 5/5] Address Harry's comment. --- lib/web_ui/test/ui/scene_builder_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/web_ui/test/ui/scene_builder_test.dart b/lib/web_ui/test/ui/scene_builder_test.dart index 03f803ec1dd7c..428437d09c12d 100644 --- a/lib/web_ui/test/ui/scene_builder_test.dart +++ b/lib/web_ui/test/ui/scene_builder_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:math'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -244,7 +243,7 @@ Future testMain() async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushOffset(50.0, 50.0); - final Matrix4 matrix = Matrix4.rotationZ(pi / 18); + final Matrix4 matrix = Matrix4.rotationZ(math.pi / 18); final ui.ImageFilter matrixFilter = ui.ImageFilter.matrix(toMatrix64(matrix.storage)); sceneBuilder.pushImageFilter(matrixFilter); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {