diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/color_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/color_filter.dart index a74145c08b..83a7ca27e2 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/color_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/color_filter.dart @@ -135,7 +135,4 @@ class EngineColorFilter implements SceneImageFilter, ui.ColorFilter { return 'ColorFilter.srgbToLinearGamma()'; } } - - @override - Matrix4? get transform => null; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart index 538b6d541b..f3658ddee3 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart @@ -5,51 +5,15 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; -import 'package:ui/src/engine.dart'; +import 'package:ui/src/engine/scene_painting.dart'; +import 'package:ui/src/engine/vector_math.dart'; import 'package:ui/ui.dart' as ui; -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 EngineRootLayer with PictureEngineLayer {} class BackdropFilterLayer with PictureEngineLayer - implements ui.BackdropFilterEngineLayer { - BackdropFilterLayer(this.operation); - - @override - final LayerOperation operation; - - @override - BackdropFilterLayer emptyClone() => BackdropFilterLayer(operation); -} + implements ui.BackdropFilterEngineLayer {} class BackdropFilterOperation implements LayerOperation { BackdropFilterOperation(this.filter, this.mode); @@ -60,12 +24,12 @@ class BackdropFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas) { - canvas.saveLayerWithFilter(ui.Rect.largest, ui.Paint()..blendMode = mode, filter); + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.saveLayerWithFilter(contentRect, ui.Paint()..blendMode = mode, filter); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { canvas.restore(); } @@ -80,15 +44,7 @@ class BackdropFilterOperation implements LayerOperation { class ClipPathLayer with PictureEngineLayer - implements ui.ClipPathEngineLayer { - ClipPathLayer(this.operation); - - @override - final ClipPathOperation operation; - - @override - ClipPathLayer emptyClone() => ClipPathLayer(operation); -} + implements ui.ClipPathEngineLayer {} class ClipPathOperation implements LayerOperation { ClipPathOperation(this.path, this.clip); @@ -99,7 +55,7 @@ class ClipPathOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(path.getBounds()); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect contentRect) { canvas.save(); canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -108,7 +64,7 @@ class ClipPathOperation implements LayerOperation { } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -126,15 +82,7 @@ class ClipPathOperation implements LayerOperation { class ClipRectLayer with PictureEngineLayer - implements ui.ClipRectEngineLayer { - ClipRectLayer(this.operation); - - @override - final ClipRectOperation operation; - - @override - ClipRectLayer emptyClone() => ClipRectLayer(operation); -} + implements ui.ClipRectEngineLayer {} class ClipRectOperation implements LayerOperation { const ClipRectOperation(this.rect, this.clip); @@ -145,7 +93,7 @@ class ClipRectOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rect); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect contentRect) { canvas.save(); canvas.clipRect(rect, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -154,7 +102,7 @@ class ClipRectOperation implements LayerOperation { } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -172,15 +120,7 @@ class ClipRectOperation implements LayerOperation { class ClipRRectLayer with PictureEngineLayer - implements ui.ClipRRectEngineLayer { - ClipRRectLayer(this.operation); - - @override - final ClipRRectOperation operation; - - @override - ClipRRectLayer emptyClone() => ClipRRectLayer(operation); -} + implements ui.ClipRRectEngineLayer {} class ClipRRectOperation implements LayerOperation { const ClipRRectOperation(this.rrect, this.clip); @@ -191,7 +131,7 @@ class ClipRRectOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rrect.outerRect); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect contentRect) { canvas.save(); canvas.clipRRect(rrect, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -200,7 +140,7 @@ class ClipRRectOperation implements LayerOperation { } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -218,15 +158,7 @@ class ClipRRectOperation implements LayerOperation { class ColorFilterLayer with PictureEngineLayer - implements ui.ColorFilterEngineLayer { - ColorFilterLayer(this.operation); - - @override - final ColorFilterOperation operation; - - @override - ColorFilterLayer emptyClone() => ColorFilterLayer(operation); -} + implements ui.ColorFilterEngineLayer {} class ColorFilterOperation implements LayerOperation { ColorFilterOperation(this.filter); @@ -236,12 +168,12 @@ class ColorFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas) { - canvas.saveLayer(ui.Rect.largest, ui.Paint()..colorFilter = filter); + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.saveLayer(contentRect, ui.Paint()..colorFilter = filter); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { canvas.restore(); } @@ -254,15 +186,7 @@ class ColorFilterOperation implements LayerOperation { class ImageFilterLayer with PictureEngineLayer - implements ui.ImageFilterEngineLayer { - ImageFilterLayer(this.operation); - - @override - final ImageFilterOperation operation; - - @override - ImageFilterLayer emptyClone() => ImageFilterLayer(operation); -} + implements ui.ImageFilterEngineLayer {} class ImageFilterOperation implements LayerOperation { ImageFilterOperation(this.filter, this.offset); @@ -273,16 +197,17 @@ class ImageFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => filter.filterBounds(contentRect); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect contentRect) { if (offset != ui.Offset.zero) { canvas.save(); canvas.translate(offset.dx, offset.dy); } - canvas.saveLayer(ui.Rect.largest, ui.Paint()..imageFilter = filter); + final ui.Rect adjustedContentRect = filter.filterBounds(contentRect); + canvas.saveLayer(adjustedContentRect, ui.Paint()..imageFilter = filter); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { if (offset != ui.Offset.zero) { canvas.restore(); } @@ -291,22 +216,13 @@ class ImageFilterOperation implements LayerOperation { @override PlatformViewStyling createPlatformViewStyling() { - PlatformViewStyling styling = const PlatformViewStyling(); if (offset != ui.Offset.zero) { - styling = PlatformViewStyling( + return 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 @@ -315,15 +231,7 @@ class ImageFilterOperation implements LayerOperation { class OffsetLayer with PictureEngineLayer - implements ui.OffsetEngineLayer { - OffsetLayer(this.operation); - - @override - final OffsetOperation operation; - - @override - OffsetLayer emptyClone() => OffsetLayer(operation); -} + implements ui.OffsetEngineLayer {} class OffsetOperation implements LayerOperation { OffsetOperation(this.dx, this.dy); @@ -334,13 +242,13 @@ class OffsetOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(ui.Offset(dx, dy)); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect cullRect) { canvas.save(); canvas.translate(dx, dy); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { canvas.restore(); } @@ -355,15 +263,7 @@ class OffsetOperation implements LayerOperation { class OpacityLayer with PictureEngineLayer - implements ui.OpacityEngineLayer { - OpacityLayer(this.operation); - - @override - final OpacityOperation operation; - - @override - OpacityLayer emptyClone() => OpacityLayer(operation); -} + implements ui.OpacityEngineLayer {} class OpacityOperation implements LayerOperation { OpacityOperation(this.alpha, this.offset); @@ -374,19 +274,20 @@ class OpacityOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(offset); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect cullRect) { if (offset != ui.Offset.zero) { canvas.save(); canvas.translate(offset.dx, offset.dy); + cullRect = cullRect.shift(-offset); } canvas.saveLayer( - ui.Rect.largest, + cullRect, ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0) ); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { canvas.restore(); if (offset != ui.Offset.zero) { canvas.restore(); @@ -405,15 +306,7 @@ class OpacityOperation implements LayerOperation { class TransformLayer with PictureEngineLayer - implements ui.TransformEngineLayer { - TransformLayer(this.operation); - - @override - final TransformOperation operation; - - @override - TransformLayer emptyClone() => TransformLayer(operation); -} + implements ui.TransformEngineLayer {} class TransformOperation implements LayerOperation { TransformOperation(this.transform); @@ -426,13 +319,13 @@ class TransformOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => matrix.transformRect(contentRect); @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect cullRect) { canvas.save(); canvas.transform(transform); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { canvas.restore(); } @@ -447,15 +340,7 @@ class TransformOperation implements LayerOperation { class ShaderMaskLayer with PictureEngineLayer - implements ui.ShaderMaskEngineLayer { - ShaderMaskLayer(this.operation); - - @override - final ShaderMaskOperation operation; - - @override - ShaderMaskLayer emptyClone() => ShaderMaskLayer(operation); -} + implements ui.ShaderMaskEngineLayer {} class ShaderMaskOperation implements LayerOperation { ShaderMaskOperation(this.shader, this.maskRect, this.blendMode); @@ -467,15 +352,15 @@ class ShaderMaskOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas) { + void pre(SceneCanvas canvas, ui.Rect contentRect) { canvas.saveLayer( - ui.Rect.largest, + contentRect, ui.Paint(), ); } @override - void post(SceneCanvas canvas) { + void post(SceneCanvas canvas, ui.Rect contentRect) { canvas.save(); canvas.translate(maskRect.left, maskRect.top); canvas.drawRect( @@ -504,43 +389,47 @@ class PlatformView { final ui.Rect bounds; final PlatformViewStyling styling; - - @override - String toString() { - return 'PlatformView(viewId: $viewId, bounds: $bounds, styling: $styling)'; - } } -class LayerSlice { - LayerSlice(this.picture, this.platformViews); +sealed class LayerSlice { + void dispose(); +} + +// A slice that contains one or more platform views to be rendered. +class PlatformViewSlice implements LayerSlice { + PlatformViewSlice(this.views, this.occlusionRect); + + List views; + + // 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; + + @override + void dispose() {} +} + +// A slice that contains flutter content to be rendered int he form of a single +// ScenePicture. +class PictureSlice implements LayerSlice { + PictureSlice(this.picture); - // The picture of native flutter content to be rendered ScenePicture picture; - // Platform views to be placed on top of the flutter content. - final List platformViews; - - void dispose() { - picture.dispose(); - } + @override + void dispose() => picture.dispose(); } 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 = []; - - List drawCommands = []; - PlatformViewStyling platformViewStyling = const PlatformViewStyling(); - - LayerOperation get operation; - - PictureEngineLayer emptyClone(); + // 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 = []; @override void dispose() { - for (final LayerSlice? slice in slices) { - slice?.dispose(); + for (final LayerSlice slice in slices) { + slice.dispose(); } } } @@ -553,8 +442,8 @@ abstract class LayerOperation { // layer operation. ui.Rect mapRect(ui.Rect contentRect); - void pre(SceneCanvas canvas); - void post(SceneCanvas canvas); + void pre(SceneCanvas canvas, ui.Rect contentRect); + void post(SceneCanvas canvas, ui.Rect contentRect); PlatformViewStyling createPlatformViewStyling(); @@ -564,29 +453,11 @@ abstract class LayerOperation { bool get shouldDrawIfEmpty; } -sealed class LayerDrawCommand { -} +class PictureDrawCommand { + PictureDrawCommand(this.offset, this.picture); -class PictureDrawCommand extends LayerDrawCommand { - PictureDrawCommand(this.offset, this.picture, this.sliceIndex); - - 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; + ui.Offset offset; + ui.Picture picture; } // Represents how a platform view should be positioned in the scene. @@ -606,17 +477,6 @@ 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; @@ -667,17 +527,6 @@ 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 @@ -696,10 +545,6 @@ 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) { @@ -730,11 +575,6 @@ 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 { @@ -802,7 +642,7 @@ class PlatformViewNoClip implements PlatformViewClip { ui.Rect get innerRect => ui.Rect.zero; @override - ui.Rect get outerRect => ui.Rect.largest; + ui.Rect get outerRect => ui.Rect.zero; } class PlatformViewRectClip implements PlatformViewClip { @@ -923,137 +763,164 @@ 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()); + return LayerBuilder._(null, EngineRootLayer(), null); } factory LayerBuilder.childLayer({ required LayerBuilder parent, required PictureEngineLayer layer, + required LayerOperation operation }) { - return LayerBuilder._(parent, layer); + return LayerBuilder._(parent, layer, operation); } LayerBuilder._( this.parent, - this.layer); + this.layer, + this.operation); + + @visibleForTesting + static (ui.PictureRecorder, SceneCanvas) Function(ui.Rect)? debugRecorderFactory; final LayerBuilder? parent; final PictureEngineLayer layer; - - final List sliceBuilders = []; - final List drawCommands = []; + final LayerOperation? operation; + final List pendingPictures = []; + List pendingPlatformViews = []; + ui.Rect? picturesRect; + ui.Rect? platformViewRect; PlatformViewStyling? _memoizedPlatformViewStyling; + PlatformViewStyling get platformViewStyling { - return _memoizedPlatformViewStyling ??= layer.operation.createPlatformViewStyling(); + return _memoizedPlatformViewStyling ??= operation?.createPlatformViewStyling() ?? const PlatformViewStyling(); } - PlatformViewStyling? _memoizedGlobalPlatformViewStyling; - PlatformViewStyling get globalPlatformViewStyling { - if (_memoizedGlobalPlatformViewStyling != null) { - return _memoizedGlobalPlatformViewStyling!; + (ui.PictureRecorder, SceneCanvas) _createRecorder(ui.Rect rect) { + if (debugRecorderFactory != null) { + return debugRecorderFactory!(rect); } - if (parent != null) { - return _memoizedGlobalPlatformViewStyling ??= PlatformViewStyling.combine(parent!.globalPlatformViewStyling, platformViewStyling); - } - return _memoizedGlobalPlatformViewStyling ??= platformViewStyling; + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; + return (recorder, canvas); } - LayerSliceBuilder getOrCreateSliceBuilderAtIndex(int index) { - while (sliceBuilders.length <= index) { - sliceBuilders.add(null); + 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)); } - final LayerSliceBuilder? existingSliceBuilder = sliceBuilders[index]; - if (existingSliceBuilder != null) { - return existingSliceBuilder; + + 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 newSliceBuilder = LayerSliceBuilder(); - layer.operation.pre(newSliceBuilder.canvas); - sliceBuilders[index] = newSliceBuilder; - return newSliceBuilder; + + pendingPictures.clear(); + pendingPlatformViews = []; + + // All the pictures and platform views have been lowered into slices. Clear + // our occlusion rectangles. + picturesRect = null; + platformViewRect = null; } void addPicture( ui.Offset offset, ui.Picture picture, { - required int sliceIndex, + bool isComplexHint = false, + bool willChangeHint = false }) { - 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); + 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(); + } } - drawCommands.add(PictureDrawCommand(offset, picture as ScenePicture, sliceIndex)); + pendingPictures.add(PictureDrawCommand(offset, picture)); + picturesRect = picturesRect?.expandToInclude(shiftedRect) ?? shiftedRect; } void addPlatformView( int viewId, { - required ui.Rect bounds, - required int sliceIndex, + ui.Offset offset = ui.Offset.zero, + double width = 0.0, + double height = 0.0 }) { - final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); - sliceBuilder.platformViews.add(PlatformView(viewId, bounds, platformViewStyling)); - drawCommands.add(PlatformViewDrawCommand(viewId, bounds, 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)); } void mergeLayer(PictureEngineLayer layer) { - 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)); - })); + // 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)); + } } } - drawCommands.add(RetainedLayerDrawCommand(layer)); - } - - 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(); + // Lower any pending pictures or platform views to their respective slices. + flushSlices(); + return layer; } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart index 6d6db06e51..af84cb0099 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart @@ -2,7 +2,6 @@ // 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'; @@ -63,115 +62,17 @@ 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 pictures. - for (final LayerSlice? slice in rootLayer.slices) { - if (slice != null) { - canvas.drawPicture(slice.picture); - } + // Only rasterizes the picture slices. + for (final PictureSlice slice in rootLayer.slices.whereType()) { + 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. @@ -185,44 +86,15 @@ class EngineSceneBuilder implements ui.SceneBuilder { bool isComplexHint = false, bool willChangeHint = false }) { - final int sliceIndex = _placePicture(offset, picture as ScenePicture); currentBuilder.addPicture( offset, picture, - sliceIndex: sliceIndex, + isComplexHint: + isComplexHint, + willChangeHint: willChangeHint ); } - // 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, { @@ -230,94 +102,17 @@ 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, - bounds: platformViewRect, - sliceIndex: sliceIndex, + offset: offset, + width: width, + height: height ); } - // 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) { - 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; + currentBuilder.mergeLayer(retainedLayer as PictureEngineLayer); } @override @@ -337,21 +132,30 @@ 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( @@ -359,14 +163,20 @@ 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( @@ -374,7 +184,8 @@ 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 @@ -382,14 +193,19 @@ 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, @@ -398,14 +214,18 @@ 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( @@ -440,10 +260,11 @@ class EngineSceneBuilder implements ui.SceneBuilder { currentBuilder.mergeLayer(layer); } - T pushLayer(T layer) { + T pushLayer(T layer, LayerOperation operation) { currentBuilder = LayerBuilder.childLayer( parent: currentBuilder, layer: layer, + operation: operation ); return layer; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart index 4f70633328..1ef39d2448 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart @@ -4,8 +4,6 @@ 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. @@ -24,10 +22,6 @@ 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/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart index 909c386041..53504204a4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart @@ -95,24 +95,23 @@ 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 == 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)); + 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)); + } } } final Map renderMap; @@ -133,55 +132,58 @@ class EngineSceneView { final List reusableContainers = List.from(containers); final List newContainers = []; - 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; + 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; } - } - - 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; + 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); } - } - container ??= PlatformViewContainer(view.viewId); - container.bounds = view.bounds; - container.styling = view.styling; - container.updateContents(); - newContainers.add(container); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart index 06b8c47cc3..7c611d110b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart @@ -88,9 +88,6 @@ class SkwasmBlurFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.blur($sigmaX, $sigmaY, ${tileModeString(tileMode)})'; - - @override - Matrix4? get transform => null; } class SkwasmDilateFilter extends SkwasmImageFilter { @@ -108,9 +105,6 @@ class SkwasmDilateFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.dilate($radiusX, $radiusY)'; - - @override - Matrix4? get transform => null; } class SkwasmErodeFilter extends SkwasmImageFilter { @@ -128,9 +122,6 @@ class SkwasmErodeFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.erode($radiusX, $radiusY)'; - - @override - Matrix4? get transform => null; } class SkwasmMatrixFilter extends SkwasmImageFilter { @@ -153,9 +144,6 @@ class SkwasmMatrixFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.matrix($matrix4, $filterQuality)'; - - @override - Matrix4? get transform => Matrix4.fromFloat32List(toMatrix32(matrix4)); } class SkwasmColorImageFilter extends SkwasmImageFilter { @@ -174,9 +162,6 @@ class SkwasmColorImageFilter extends SkwasmImageFilter { @override String toString() => filter.toString(); - - @override - Matrix4? get transform => null; } class SkwasmComposedImageFilter extends SkwasmImageFilter { @@ -198,16 +183,6 @@ 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/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart index a94a65159b..d8826e8da6 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart @@ -16,7 +16,7 @@ void main() { void testMain() { setUpAll(() { - LayerSliceBuilder.debugRecorderFactory = () { + LayerBuilder.debugRecorderFactory = (ui.Rect rect) { final StubSceneCanvas canvas = StubSceneCanvas(); final StubPictureRecorder recorder = StubPictureRecorder(canvas); return (recorder, canvas); @@ -24,7 +24,7 @@ void testMain() { }); tearDownAll(() { - LayerSliceBuilder.debugRecorderFactory = null; + LayerBuilder.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], layerSlice(withPictureRect: pictureRect)); + expect(slices[0], pictureSliceWithRect(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, 300, 400, 400); + const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 400, 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], layerSlice(withPictureRect: const ui.Rect.fromLTRB(100, 100, 400, 400))); + expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(100, 100, 400, 400))); }); test('picture + platform view (overlapping)', () { @@ -68,11 +68,10 @@ void testMain() { ); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 1); - expect(slices[0], layerSlice( - withPictureRect: pictureRect, - withPlatformViews: [ + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], pictureSliceWithRect(pictureRect)); + expect(slices[1], platformViewSliceWithViews([ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); }); @@ -91,12 +90,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], layerSlice(withPlatformViews: [ + expect(slices[0], platformViewSliceWithViews([ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); - expect(slices[1], layerSlice(withPictureRect: pictureRect)); + expect(slices[1], pictureSliceWithRect(pictureRect)); }); test('platform view sandwich (overlapping)', () { @@ -115,14 +114,13 @@ 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, 2); - expect(slices[0], layerSlice( - withPictureRect: pictureRect1, - withPlatformViews: [ + final List slices = scene.rootLayer.slices; + expect(slices.length, 3); + expect(slices[0], pictureSliceWithRect(pictureRect1)); + expect(slices[1], platformViewSliceWithViews([ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); - expect(slices[1], layerSlice(withPictureRect: pictureRect2)); + expect(slices[2], pictureSliceWithRect(pictureRect2)); }); test('platform view sandwich (non-overlapping)', () { @@ -141,15 +139,14 @@ 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, 1); - expect(slices[0], layerSlice( - withPictureRect: const ui.Rect.fromLTRB(50, 50, 200, 200), - withPlatformViews: [ + expect(slices.length, 2); + expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(50, 50, 200, 200))); + expect(slices[1], platformViewSliceWithViews([ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); }); @@ -172,99 +169,34 @@ 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, 2); - expect(slices[0], layerSlice( - withPictureRect: pictureRect1, - withPlatformViews: [ + final List slices = scene.rootLayer.slices; + expect(slices.length, 3); + expect(slices[0], pictureSliceWithRect(pictureRect1)); + expect(slices[1], platformViewSliceWithViews([ PlatformView(1, platformViewRect, const PlatformViewStyling(position: PlatformViewPosition.offset(ui.Offset(150, 150)))) ])); - 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, - ))); + expect(slices[2], pictureSliceWithRect(const ui.Rect.fromLTRB(200, 200, 300, 300))); }); }); } -LayerSliceMatcher layerSlice({ - ui.Rect withPictureRect = ui.Rect.zero, - List withPlatformViews = const [], -}) => LayerSliceMatcher(withPictureRect, withPlatformViews); -class LayerSliceMatcher extends Matcher { - LayerSliceMatcher(this.expectedPictureRect, this.expectedPlatformViews); +PictureSliceMatcher pictureSliceWithRect(ui.Rect rect) => PictureSliceMatcher(rect); +PlatformViewSliceMatcher platformViewSliceWithViews(List views) + => PlatformViewSliceMatcher(views); - final ui.Rect expectedPictureRect; - final List expectedPlatformViews; +class PictureSliceMatcher extends Matcher { + PictureSliceMatcher(this.expectedRect); + + final ui.Rect expectedRect; @override Description describe(Description description) { - return description.add(''); + return description.add(''); } @override bool matches(dynamic item, Map matchState) { - if (item is! LayerSlice) { + if (item is! PictureSlice) { return false; } final ScenePicture picture = item.picture; @@ -272,28 +204,50 @@ class LayerSliceMatcher extends Matcher { return false; } - if (picture.cullRect != expectedPictureRect) { + if (picture.cullRect != expectedRect) { return false; } - if (item.platformViews.length != expectedPlatformViews.length) { - return false; - } - - for (int i = 0; i < item.platformViews.length; i++) { - final PlatformView expectedView = expectedPlatformViews[i]; - final PlatformView actualView = item.platformViews[i]; - if (expectedView.viewId != actualView.viewId) { - return false; - } - if (expectedView.bounds != actualView.bounds) { - return false; - } - if (expectedView.styling != actualView.styling) { - 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) { + return false; + } + + if (item.views.length != expectedPlatformViews.length) { + return false; + } + + for (int i = 0; i < item.views.length; i++) { + final PlatformView expectedView = expectedPlatformViews[i]; + final PlatformView actualView = item.views[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/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart index ec014e7054..033177c8d7 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart @@ -36,12 +36,8 @@ 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/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart index 58ff09de66..80093bb031 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart @@ -172,7 +172,7 @@ void testMain() { 120, )); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(LayerSlice(picture, [])); + rootLayer.slices.add(PictureSlice(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(LayerSlice(StubPicture(ui.Rect.zero), [platformView])); + rootLayer.slices.add(PlatformViewSlice([platformView], null)); 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(LayerSlice(picture, [])); + rootLayer.slices.add(PictureSlice(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(LayerSlice(picture, [])); + rootLayer.slices.add(PictureSlice(picture)); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); diff --git a/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart b/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart index 428437d09c..8199b1fd0b 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart @@ -223,7 +223,7 @@ Future testMain() async { region: region); }); - test('blur image filter layer', () async { + test('image filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushImageFilter(ui.ImageFilter.blur( sigmaX: 5.0, @@ -239,23 +239,6 @@ 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(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) { - 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();