diff --git a/engine/src/flutter/lib/ui/compositing.dart b/engine/src/flutter/lib/ui/compositing.dart index 6cdd8e260cd..357952087e2 100644 --- a/engine/src/flutter/lib/ui/compositing.dart +++ b/engine/src/flutter/lib/ui/compositing.dart @@ -38,6 +38,137 @@ class Scene extends NativeFieldWrapperClass2 { void dispose() native 'Scene_dispose'; } +// Lightweight wrapper of a native layer object. +// +// This is used to provide a typed API for engine layers to prevent +// incompatible layers from being passed to [SceneBuilder]'s push methods. +// For example, this prevents a layer returned from `pushOpacity` from being +// passed as `oldLayer` to `pushTransform`. This is achieved by having one +// concrete subclass of this class per push method. +abstract class _EngineLayerWrapper implements EngineLayer { + _EngineLayerWrapper._(this._nativeLayer); + + EngineLayer _nativeLayer; + + // Children of this layer. + // + // Null if this layer has no children. This field is populated only in debug + // mode. + List<_EngineLayerWrapper> _debugChildren; + + // Whether this layer was used as `oldLayer` in a past frame. + // + // It is illegal to use a layer object again after it is passed as an + // `oldLayer` argument. + bool _debugWasUsedAsOldLayer = false; + + bool _debugCheckNotUsedAsOldLayer() { + assert( + !_debugWasUsedAsOldLayer, + 'Layer $runtimeType was previously used as oldLayer.\n' + 'Once a layer is used as oldLayer, it may not be used again. Instead, ' + 'after calling one of the SceneBuilder.push* methods and passing an oldLayer ' + 'to it, use the layer returned by the method as oldLayer in subsequent ' + 'frames.' + ); + return true; + } +} + +/// An opaque handle to a transform engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushTransform]. +/// +/// {@template dart.ui.sceneBuilder.oldLayerCompatibility} +/// `oldLayer` parameter in [SceneBuilder] methods only accepts objects created +/// by the engine. [SceneBuilder] will throw an [AssertionError] if you pass it +/// a custom implementation of this class. +/// {@endtemplate} +class TransformEngineLayer extends _EngineLayerWrapper { + TransformEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to an offset engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushOffset]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class OffsetEngineLayer extends _EngineLayerWrapper { + OffsetEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a clip rect engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushClipRect]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class ClipRectEngineLayer extends _EngineLayerWrapper { + ClipRectEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a clip rounded rect engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushClipRRect]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class ClipRRectEngineLayer extends _EngineLayerWrapper { + ClipRRectEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a clip path engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushClipPath]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class ClipPathEngineLayer extends _EngineLayerWrapper { + ClipPathEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to an opacity engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushOpacity]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class OpacityEngineLayer extends _EngineLayerWrapper { + OpacityEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a color filter engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushColorFilter]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class ColorFilterEngineLayer extends _EngineLayerWrapper { + ColorFilterEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a backdrop filter engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushBackdropFilter]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class BackdropFilterEngineLayer extends _EngineLayerWrapper { + BackdropFilterEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a shader mask engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushShaderMask]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class ShaderMaskEngineLayer extends _EngineLayerWrapper { + ShaderMaskEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + +/// An opaque handle to a physical shape engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushPhysicalShape]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class PhysicalShapeEngineLayer extends _EngineLayerWrapper { + PhysicalShapeEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer); +} + /// Builds a [Scene] containing the given visuals. /// /// A [Scene] can then be rendered using [Window.render]. @@ -51,14 +182,97 @@ class SceneBuilder extends NativeFieldWrapperClass2 { SceneBuilder() { _constructor(); } void _constructor() native 'SceneBuilder_constructor'; + // Layers used in this scene. + // + // The key is the layer used. The value is the description of what the layer + // is used for, e.g. "pushOpacity" or "addRetained". + Map _usedLayers = {}; + + // In debug mode checks that the `layer` is only used once in a given scene. + bool _debugCheckUsedOnce(EngineLayer layer, String usage) { + assert(() { + if (layer == null) { + return true; + } + + assert( + !_usedLayers.containsKey(layer), + 'Layer ${layer.runtimeType} already used.\n' + 'The layer is already being used as ${_usedLayers[layer]} in this scene.\n' + 'A layer may only be used once in a given scene.' + ); + + _usedLayers[layer] = usage; + return true; + }()); + + return true; + } + + bool _debugCheckCanBeUsedAsOldLayer(_EngineLayerWrapper layer, String methodName) { + assert(() { + if (layer == null) { + return true; + } + layer._debugCheckNotUsedAsOldLayer(); + assert(_debugCheckUsedOnce(layer, 'oldLayer in $methodName')); + layer._debugWasUsedAsOldLayer = true; + return true; + }()); + return true; + } + + final List<_EngineLayerWrapper> _layerStack = <_EngineLayerWrapper>[]; + + // Pushes the `newLayer` onto the `_layerStack` and adds it to the + // `_debugChildren` of the current layer in the stack, if any. + bool _debugPushLayer(_EngineLayerWrapper newLayer) { + assert(() { + if (_layerStack.isNotEmpty) { + final _EngineLayerWrapper currentLayer = _layerStack.last; + currentLayer._debugChildren ??= <_EngineLayerWrapper>[]; + currentLayer._debugChildren.add(newLayer); + } + _layerStack.add(newLayer); + return true; + }()); + return true; + } + /// Pushes a transform operation onto the operation stack. /// /// The objects are transformed by the given matrix before rasterization. /// + /// {@template dart.ui.sceneBuilder.oldLayer} + /// If `oldLayer` is not null the engine will attempt to reuse the resources + /// allocated for the old layer when rendering the new layer. This is purely + /// an optimization. It has no effect on the correctness of rendering. + /// {@endtemplate} + /// + /// {@template dart.ui.sceneBuilder.oldLayerVsRetained} + /// Passing a layer to [addRetained] or as `oldLayer` argument to a push + /// method counts as _usage_. A layer can be used no more than once in a scene. + /// For example, it may not be passed simultaneously to two push methods, or + /// to a push method and to `addRetained`. + /// + /// When a layer is passed to [addRetained] all descendant layers are also + /// considered as used in this scene. The same single-usage restriction + /// applies to descendants. + /// + /// When a layer is passed as an `oldLayer` argument to a push method, it may + /// no longer be used in subsequent frames. If you would like to continue + /// reusing the resources associated with the layer, store the layer object + /// returned by the push method and use that in the next frame instead of the + /// original object. + /// {@endtemplate} + /// /// See [pop] for details about the operation stack. - EngineLayer pushTransform(Float64List matrix4) { + TransformEngineLayer pushTransform(Float64List matrix4, { TransformEngineLayer oldLayer }) { assert(_matrix4IsValid(matrix4)); - return _pushTransform(matrix4); + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushTransform')); + final TransformEngineLayer layer = TransformEngineLayer._(_pushTransform(matrix4)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushTransform(Float64List matrix4) native 'SceneBuilder_pushTransform'; @@ -66,19 +280,36 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// /// This is equivalent to [pushTransform] with a matrix with only translation. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack. - EngineLayer pushOffset(double dx, double dy) native 'SceneBuilder_pushOffset'; + OffsetEngineLayer pushOffset(double dx, double dy, { OffsetEngineLayer oldLayer }) { + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushOffset')); + final OffsetEngineLayer layer = OffsetEngineLayer._(_pushOffset(dx, dy)); + assert(_debugPushLayer(layer)); + return layer; + } + EngineLayer _pushOffset(double dx, double dy) native 'SceneBuilder_pushOffset'; /// Pushes a rectangular clip operation onto the operation stack. /// /// Rasterization outside the given rectangle is discarded. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack, and [Clip] for different clip modes. /// By default, the clip will be anti-aliased (clip = [Clip.antiAlias]). - EngineLayer pushClipRect(Rect rect, {Clip clipBehavior = Clip.antiAlias}) { + ClipRectEngineLayer pushClipRect(Rect rect, {Clip clipBehavior = Clip.antiAlias, ClipRectEngineLayer oldLayer }) { assert(clipBehavior != null); assert(clipBehavior != Clip.none); - return _pushClipRect(rect.left, rect.right, rect.top, rect.bottom, clipBehavior.index); + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushClipRect')); + final ClipRectEngineLayer layer = ClipRectEngineLayer._(_pushClipRect(rect.left, rect.right, rect.top, rect.bottom, clipBehavior.index)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushClipRect(double left, double right, @@ -90,12 +321,19 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// /// Rasterization outside the given rounded rectangle is discarded. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack, and [Clip] for different clip modes. /// By default, the clip will be anti-aliased (clip = [Clip.antiAlias]). - EngineLayer pushClipRRect(RRect rrect, {Clip clipBehavior = Clip.antiAlias}) { + ClipRRectEngineLayer pushClipRRect(RRect rrect, {Clip clipBehavior = Clip.antiAlias, ClipRRectEngineLayer oldLayer}) { assert(clipBehavior != null); assert(clipBehavior != Clip.none); - return _pushClipRRect(rrect._value32, clipBehavior.index); + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushClipRRect')); + final ClipRRectEngineLayer layer = ClipRRectEngineLayer._(_pushClipRRect(rrect._value32, clipBehavior.index)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushClipRRect(Float32List rrect, int clipBehavior) native 'SceneBuilder_pushClipRRect'; @@ -103,12 +341,19 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// /// Rasterization outside the given path is discarded. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack. See [Clip] for different clip modes. /// By default, the clip will be anti-aliased (clip = [Clip.antiAlias]). - EngineLayer pushClipPath(Path path, {Clip clipBehavior = Clip.antiAlias}) { + ClipPathEngineLayer pushClipPath(Path path, {Clip clipBehavior = Clip.antiAlias, ClipPathEngineLayer oldLayer}) { assert(clipBehavior != null); assert(clipBehavior != Clip.none); - return _pushClipPath(path, clipBehavior.index); + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushClipPath')); + final ClipPathEngineLayer layer = ClipPathEngineLayer._(_pushClipPath(path, clipBehavior.index)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushClipPath(Path path, int clipBehavior) native 'SceneBuilder_pushClipPath'; @@ -119,9 +364,16 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// An alpha value of 255 has no effect (i.e., the objects retain the current /// opacity). /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack. - EngineLayer pushOpacity(int alpha, {Offset offset = Offset.zero}) { - return _pushOpacity(alpha, offset.dx, offset.dy); + OpacityEngineLayer pushOpacity(int alpha, {Offset offset = Offset.zero, OpacityEngineLayer oldLayer}) { + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushOpacity')); + final OpacityEngineLayer layer = OpacityEngineLayer._(_pushOpacity(alpha, offset.dx, offset.dy)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushOpacity(int alpha, double dx, double dy) native 'SceneBuilder_pushOpacity'; @@ -130,9 +382,16 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// The given color is applied to the objects' rasterization using the given /// blend mode. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack. - EngineLayer pushColorFilter(Color color, BlendMode blendMode) { - return _pushColorFilter(color.value, blendMode.index); + ColorFilterEngineLayer pushColorFilter(Color color, BlendMode blendMode, { ColorFilterEngineLayer oldLayer }) { + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushColorFilter')); + final ColorFilterEngineLayer layer = ColorFilterEngineLayer._(_pushColorFilter(color.value, blendMode.index)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushColorFilter(int color, int blendMode) native 'SceneBuilder_pushColorFilter'; @@ -141,22 +400,39 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// The given filter is applied to the current contents of the scene prior to /// rasterizing the given objects. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack. - EngineLayer pushBackdropFilter(ImageFilter filter) native 'SceneBuilder_pushBackdropFilter'; + BackdropFilterEngineLayer pushBackdropFilter(ImageFilter filter, { BackdropFilterEngineLayer oldLayer }) { + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushBackdropFilter')); + final BackdropFilterEngineLayer layer = BackdropFilterEngineLayer._(_pushBackdropFilter(filter)); + assert(_debugPushLayer(layer)); + return layer; + } + EngineLayer _pushBackdropFilter(ImageFilter filter) native 'SceneBuilder_pushBackdropFilter'; /// Pushes a shader mask operation onto the operation stack. /// /// The given shader is applied to the object's rasterization in the given /// rectangle using the given blend mode. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack. - EngineLayer pushShaderMask(Shader shader, Rect maskRect, BlendMode blendMode) { - return _pushShaderMask(shader, + ShaderMaskEngineLayer pushShaderMask(Shader shader, Rect maskRect, BlendMode blendMode, { ShaderMaskEngineLayer oldLayer }) { + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushShaderMask')); + final ShaderMaskEngineLayer layer = ShaderMaskEngineLayer._(_pushShaderMask(shader, maskRect.left, maskRect.right, maskRect.top, maskRect.bottom, - blendMode.index); + blendMode.index)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushShaderMask(Shader shader, double maskRectLeft, @@ -176,10 +452,17 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// [shadowColor] defines the color of the shadow if present and [color] defines the /// color of the layer background. /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// /// See [pop] for details about the operation stack, and [Clip] for different clip modes. // ignore: deprecated_member_use - EngineLayer pushPhysicalShape({ Path path, double elevation, Color color, Color shadowColor, Clip clipBehavior = Clip.none}) { - return _pushPhysicalShape(path, elevation, color.value, shadowColor?.value ?? 0xFF000000, clipBehavior.index); + PhysicalShapeEngineLayer pushPhysicalShape({ Path path, double elevation, Color color, Color shadowColor, Clip clipBehavior = Clip.none, PhysicalShapeEngineLayer oldLayer }) { + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushPhysicalShape')); + final PhysicalShapeEngineLayer layer = PhysicalShapeEngineLayer._(_pushPhysicalShape(path, elevation, color.value, shadowColor?.value ?? 0xFF000000, clipBehavior.index)); + assert(_debugPushLayer(layer)); + return layer; } EngineLayer _pushPhysicalShape(Path path, double elevation, int color, int shadowColor, int clipBehavior) native 'SceneBuilder_pushPhysicalShape'; @@ -190,7 +473,13 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// operations in the stack applies to each of the objects added to the scene. /// Calling this function removes the most recently added operation from the /// stack. - void pop() native 'SceneBuilder_pop'; + void pop() { + if (_layerStack.isNotEmpty) { + _layerStack.removeLast(); + } + _pop(); + } + void _pop() native 'SceneBuilder_pop'; /// Add a retained engine layer subtree from previous frames. /// @@ -200,7 +489,33 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// Therefore, when implementing a subclass of the [Layer] concept defined in /// the rendering layer of Flutter's framework, once this is called, there's /// no need to call [addToScene] for its children layers. - void addRetained(EngineLayer retainedLayer) native 'SceneBuilder_addRetained'; + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + void addRetained(EngineLayer retainedLayer) { + assert(retainedLayer is _EngineLayerWrapper); + assert(() { + final _EngineLayerWrapper layer = retainedLayer; + + void recursivelyCheckChildrenUsedOnce(_EngineLayerWrapper parentLayer) { + _debugCheckUsedOnce(parentLayer, 'retained layer'); + parentLayer._debugCheckNotUsedAsOldLayer(); + + if (parentLayer._debugChildren == null || parentLayer._debugChildren.isEmpty) { + return; + } + + parentLayer._debugChildren.forEach(recursivelyCheckChildrenUsedOnce); + } + + recursivelyCheckChildrenUsedOnce(layer); + + return true; + }()); + + final _EngineLayerWrapper wrapper = retainedLayer; + _addRetained(wrapper._nativeLayer); + } + void _addRetained(EngineLayer retainedLayer) native 'SceneBuilder_addRetained'; /// Adds an object to the scene that displays performance statistics. /// diff --git a/engine/src/flutter/testing/dart/compositing_test.dart b/engine/src/flutter/testing/dart/compositing_test.dart index 6135260c6e9..478f0aef6b3 100644 --- a/engine/src/flutter/testing/dart/compositing_test.dart +++ b/engine/src/flutter/testing/dart/compositing_test.dart @@ -51,4 +51,250 @@ void main() { throwsA(const TypeMatcher()), ); }); + + test('SceneBuilder accepts typed layers', () { + final SceneBuilder builder1 = SceneBuilder(); + final OpacityEngineLayer opacity1 = builder1.pushOpacity(100); + expect(opacity1, isNotNull); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + final OpacityEngineLayer opacity2 = builder2.pushOpacity(200, oldLayer: opacity1); + expect(opacity2, isNotNull); + builder2.pop(); + builder2.build(); + }); + + // Attempts to use the same layer first as `oldLayer` then in `addRetained`. + void testPushThenIllegalRetain(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + pushFunction(builder2, layer); + builder2.pop(); + try { + builder2.addRetained(layer); + fail('Expected addRetained to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('The layer is already being used')); + } + builder2.build(); + } + + // Attempts to use the same layer first in `addRetained` then as `oldLayer`. + void testAddRetainedThenIllegalPush(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + builder2.addRetained(layer); + try { + pushFunction(builder2, layer); + fail('Expected push to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('The layer is already being used')); + } + builder2.build(); + } + + // Attempts to retain the same layer twice in the same scene. + void testDoubleAddRetained(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + builder2.addRetained(layer); + try { + builder2.addRetained(layer); + fail('Expected second addRetained to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('The layer is already being used')); + } + builder2.build(); + } + + // Attempts to use the same layer as `oldLayer` twice in the same scene. + void testPushOldLayerTwice(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + pushFunction(builder2, layer); + try { + pushFunction(builder2, layer); + fail('Expected push to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('was previously used as oldLayer')); + } + builder2.build(); + } + + // Attempts to use a child of a retained layer as an `oldLayer`. + void testPushChildLayerOfRetainedLayer(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + final EngineLayer childLayer = builder1.pushOpacity(123); + builder1.pop(); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + builder2.addRetained(layer); + try { + builder2.pushOpacity(321, oldLayer: childLayer); + fail('Expected pushOpacity to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('The layer is already being used')); + } + builder2.build(); + } + + // Attempts to retain a layer whose child is already used as `oldLayer` elsewhere in the scene. + void testRetainParentLayerOfPushedChild(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + final EngineLayer childLayer = builder1.pushOpacity(123); + builder1.pop(); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + builder2.pushOpacity(234, oldLayer: childLayer); + builder2.pop(); + try { + builder2.addRetained(layer); + fail('Expected addRetained to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('The layer is already being used')); + } + builder2.build(); + } + + // Attempts to retain a layer that has been used as `oldLayer` in a previous frame. + void testRetainOldLayer(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + pushFunction(builder2, layer); + builder2.pop(); + try { + final SceneBuilder builder3 = SceneBuilder(); + builder3.addRetained(layer); + fail('Expected addRetained to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('was previously used as oldLayer')); + } + builder2.build(); + } + + // Attempts to pass layer as `oldLayer` that has been used as `oldLayer` in a previous frame. + void testPushOldLayer(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer layer = pushFunction(builder1, null); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + pushFunction(builder2, layer); + builder2.pop(); + try { + final SceneBuilder builder3 = SceneBuilder(); + pushFunction(builder3, layer); + fail('Expected addRetained to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('was previously used as oldLayer')); + } + builder2.build(); + } + + // Attempts to retain a parent of a layer used as `oldLayer` in a previous frame. + void testRetainsParentOfOldLayer(_TestNoSharingFunction pushFunction) { + final SceneBuilder builder1 = SceneBuilder(); + final EngineLayer parentLayer = pushFunction(builder1, null); + final OpacityEngineLayer childLayer = builder1.pushOpacity(123); + builder1.pop(); + builder1.pop(); + builder1.build(); + + final SceneBuilder builder2 = SceneBuilder(); + builder2.pushOpacity(321, oldLayer: childLayer); + builder2.pop(); + try { + final SceneBuilder builder3 = SceneBuilder(); + builder3.addRetained(parentLayer); + fail('Expected addRetained to throw AssertionError but it returned successully'); + } on AssertionError catch (error) { + expect(error.toString(), contains('was previously used as oldLayer')); + } + builder2.build(); + } + + void testNoSharing(_TestNoSharingFunction pushFunction) { + testPushThenIllegalRetain(pushFunction); + testAddRetainedThenIllegalPush(pushFunction); + testDoubleAddRetained(pushFunction); + testPushOldLayerTwice(pushFunction); + testPushChildLayerOfRetainedLayer(pushFunction); + testRetainParentLayerOfPushedChild(pushFunction); + testRetainOldLayer(pushFunction); + testPushOldLayer(pushFunction); + testRetainsParentOfOldLayer(pushFunction); + } + + test('SceneBuilder does not share a layer between addRetained and push*', () { + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushOffset(0, 0, oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushTransform(Float64List(16), oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushClipRect(Rect.zero, oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushClipRRect(RRect.zero, oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushClipPath(Path(), oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushOpacity(100, oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushColorFilter(const Color.fromARGB(0, 0, 0, 0), BlendMode.color, oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushBackdropFilter(ImageFilter.blur(), oldLayer: oldLayer); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushShaderMask( + Gradient.radial( + const Offset(0, 0), + 10, + const [Color.fromARGB(0, 0, 0, 0), Color.fromARGB(0, 255, 255, 255)], + ), + Rect.zero, + BlendMode.color, + oldLayer: oldLayer, + ); + }); + testNoSharing((SceneBuilder builder, EngineLayer oldLayer) { + return builder.pushPhysicalShape(path: Path(), color: const Color.fromARGB(0, 0, 0, 0), oldLayer: oldLayer); + }); + }); } + +typedef _TestNoSharingFunction = EngineLayer Function(SceneBuilder builder, EngineLayer oldLayer);