Reland [skwasm] Scene builder optimizations for platform view placement (flutter/engine#55468)

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.
This commit is contained in:
Jackson Gardner 2024-09-30 10:42:58 -07:00 committed by GitHub
parent 2482050afd
commit b99e758ee1
10 changed files with 783 additions and 372 deletions

View File

@ -135,4 +135,7 @@ class EngineColorFilter implements SceneImageFilter, ui.ColorFilter {
return 'ColorFilter.srgbToLinearGamma()';
}
}
@override
Matrix4? get transform => null;
}

View File

@ -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(ui.Rect.largest, 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,7 +99,7 @@ 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) {
@ -64,7 +108,7 @@ class ClipPathOperation implements LayerOperation {
}
@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(ui.Rect.largest, 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(ui.Rect.largest, 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,
ui.Rect.largest,
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,
ui.Rect.largest,
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();
}
// A slice that contains one or more platform views to be rendered.
class PlatformViewSlice implements LayerSlice {
PlatformViewSlice(this.views, this.occlusionRect);
List<PlatformView> 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() {}
String toString() {
return 'PlatformView(viewId: $viewId, bounds: $bounds, styling: $styling)';
}
}
// A slice that contains flutter content to be rendered int he form of a single
// ScenePicture.
class PictureSlice implements LayerSlice {
PictureSlice(this.picture);
class LayerSlice {
LayerSlice(this.picture, this.platformViews);
// The picture of native flutter content to be rendered
ScenePicture picture;
@override
void dispose() => picture.dispose();
// Platform views to be placed on top of the flutter content.
final List<PlatformView> platformViews;
void dispose() {
picture.dispose();
}
}
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<LayerSlice> slices = <LayerSlice>[];
// 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<LayerSlice?> slices = [];
List<LayerDrawCommand> drawCommands = [];
PlatformViewStyling platformViewStyling = const PlatformViewStyling();
LayerOperation get operation;
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 {
}
ui.Offset offset;
ui.Picture 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;
}
// 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<PlatformView> platformViews = <PlatformView>[];
}
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<PictureDrawCommand> pendingPictures = <PictureDrawCommand>[];
List<PlatformView> pendingPlatformViews = <PlatformView>[];
ui.Rect? picturesRect;
ui.Rect? platformViewRect;
final List<LayerSliceBuilder?> sliceBuilders = <LayerSliceBuilder?>[];
final List<LayerDrawCommand> drawCommands = <LayerDrawCommand>[];
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 = <PlatformView>[];
// 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 sliceUp() {
final List<LayerSlice?> 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() {
// Lower any pending pictures or platform views to their respective slices.
flushSlices();
return layer;
layer.drawCommands = drawCommands;
layer.platformViewStyling = platformViewStyling;
return sliceUp();
}
}

View File

@ -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<PictureSlice>()) {
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<SceneSlice> sceneSlices = <SceneSlice>[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<LayerDrawCommand> 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>(
BackdropFilterLayer(),
BackdropFilterOperation(filter, blendMode),
);
}) => pushLayer<BackdropFilterLayer>(BackdropFilterLayer(BackdropFilterOperation(filter, blendMode)));
@override
ui.ClipPathEngineLayer pushClipPath(
ui.Path path, {
ui.Clip clipBehavior = ui.Clip.antiAlias,
ui.ClipPathEngineLayer? oldLayer
}) => pushLayer<ClipPathLayer>(
ClipPathLayer(),
ClipPathOperation(path as ScenePath, clipBehavior),
);
}) => pushLayer<ClipPathLayer>(ClipPathLayer(ClipPathOperation(path as ScenePath, clipBehavior)));
@override
ui.ClipRRectEngineLayer pushClipRRect(
ui.RRect rrect, {
required ui.Clip clipBehavior,
ui.ClipRRectEngineLayer? oldLayer
}) => pushLayer<ClipRRectLayer>(
ClipRRectLayer(),
ClipRRectOperation(rrect, clipBehavior)
);
}) => pushLayer<ClipRRectLayer>(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>(
ClipRectLayer(),
ClipRectOperation(rect, clipBehavior)
);
return pushLayer<ClipRectLayer>(ClipRectLayer(ClipRectOperation(rect, clipBehavior)));
}
@override
ui.ColorFilterEngineLayer pushColorFilter(
ui.ColorFilter filter, {
ui.ColorFilterEngineLayer? oldLayer
}) => pushLayer<ColorFilterLayer>(
ColorFilterLayer(),
ColorFilterOperation(filter),
);
}) => pushLayer<ColorFilterLayer>(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>(
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>(
OffsetLayer(),
OffsetOperation(dx, dy)
);
}) => pushLayer<OffsetLayer>(OffsetLayer(OffsetOperation(dx, dy)));
@override
ui.OpacityEngineLayer pushOpacity(int alpha, {
ui.Offset offset = ui.Offset.zero,
ui.OpacityEngineLayer? oldLayer
}) => pushLayer<OpacityLayer>(
OpacityLayer(),
OpacityOperation(alpha, offset),
);
}) => pushLayer<OpacityLayer>(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>(
ShaderMaskLayer(),
ShaderMaskOperation(shader, maskRect, blendMode)
ShaderMaskLayer(ShaderMaskOperation(shader, maskRect, blendMode)),
);
@override
ui.TransformEngineLayer pushTransform(
Float64List matrix4, {
ui.TransformEngineLayer? oldLayer
}) => pushLayer<TransformLayer>(
TransformLayer(),
TransformOperation(matrix4),
);
}) => pushLayer<TransformLayer>(TransformLayer(TransformOperation(matrix4)));
@override
void setProperties(
@ -260,11 +440,10 @@ class EngineSceneBuilder implements ui.SceneBuilder {
currentBuilder.mergeLayer(layer);
}
T pushLayer<T extends PictureEngineLayer>(T layer, LayerOperation operation) {
T pushLayer<T extends PictureEngineLayer>(T layer) {
currentBuilder = LayerBuilder.childLayer(
parent: currentBuilder,
layer: layer,
operation: operation
);
return layer;
}

View File

@ -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 {

View File

@ -95,23 +95,24 @@ class EngineSceneView {
flutterView.physicalSize.width,
flutterView.physicalSize.height,
);
final List<LayerSlice> slices = scene.rootLayer.slices;
final List<LayerSlice?> slices = scene.rootLayer.slices;
final List<ScenePicture> picturesToRender = <ScenePicture>[];
final List<ScenePicture> originalPicturesToRender = <ScenePicture>[];
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<ScenePicture, DomImageBitmap> renderMap;
@ -132,58 +133,55 @@ class EngineSceneView {
final List<SliceContainer?> reusableContainers = List<SliceContainer?>.from(containers);
final List<SliceContainer> newContainers = <SliceContainer>[];
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);
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);
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);
// 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);
}
}

View File

@ -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);

View File

@ -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<LayerSlice> slices = scene.rootLayer.slices;
final List<LayerSlice?> 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<LayerSlice> slices = scene.rootLayer.slices;
final List<LayerSlice?> 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<LayerSlice> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(slices[0], pictureSliceWithRect(pictureRect));
expect(slices[1], platformViewSliceWithViews(<PlatformView>[
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 1);
expect(slices[0], layerSlice(
withPictureRect: pictureRect,
withPlatformViews: <PlatformView>[
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<LayerSlice> slices = scene.rootLayer.slices;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(slices[0], platformViewSliceWithViews(<PlatformView>[
expect(slices[0], layerSlice(withPlatformViews: <PlatformView>[
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<LayerSlice> slices = scene.rootLayer.slices;
expect(slices.length, 3);
expect(slices[0], pictureSliceWithRect(pictureRect1));
expect(slices[1], platformViewSliceWithViews(<PlatformView>[
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(slices[0], layerSlice(
withPictureRect: pictureRect1,
withPlatformViews: <PlatformView>[
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<LayerSlice> slices = scene.rootLayer.slices;
final List<LayerSlice?> 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(<PlatformView>[
expect(slices.length, 1);
expect(slices[0], layerSlice(
withPictureRect: const ui.Rect.fromLTRB(50, 50, 200, 200),
withPlatformViews: <PlatformView>[
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<LayerSlice> slices = scene.rootLayer.slices;
expect(slices.length, 3);
expect(slices[0], pictureSliceWithRect(pictureRect1));
expect(slices[1], platformViewSliceWithViews(<PlatformView>[
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(slices[0], layerSlice(
withPictureRect: pictureRect1,
withPlatformViews: <PlatformView>[
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<PlatformView> expectedPlatformViews = <PlatformView>[];
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<LayerSlice?> 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<PlatformView> views)
=> PlatformViewSliceMatcher(views);
LayerSliceMatcher layerSlice({
ui.Rect withPictureRect = ui.Rect.zero,
List<PlatformView> withPlatformViews = const <PlatformView>[],
}) => 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<PlatformView> expectedPlatformViews;
@override
Description describe(Description description) {
return description.add('<picture slice with cullRect: $expectedRect>');
return description.add('<picture slice with cullRect: $expectedPictureRect and platform views: $expectedPlatformViews>');
}
@override
bool matches(dynamic item, Map<dynamic, dynamic> 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<PlatformView> expectedPlatformViews;
@override
Description describe(Description description) {
return description.add('<platform view slice with platform views: $expectedPlatformViews>');
}
@override
bool matches(dynamic item, Map<dynamic, dynamic> 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;
}
}

View File

@ -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<StubPicture> children;

View File

@ -172,7 +172,7 @@ void testMain() {
120,
));
final EngineRootLayer rootLayer = EngineRootLayer();
rootLayer.slices.add(PictureSlice(picture));
rootLayer.slices.add(LayerSlice(picture, <PlatformView>[]));
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>[platformView], null));
rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), <PlatformView>[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, <PlatformView>[]));
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, <PlatformView>[]));
final EngineScene scene = EngineScene(rootLayer);
await sceneView.renderScene(scene, null);

View File

@ -243,7 +243,7 @@ Future<void> 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 +262,23 @@ Future<void> 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();