Unverified Commit 025397ae authored by Dan Field's avatar Dan Field Committed by GitHub

Release retained resources from layers/dispose pictures (#84740)

parent 1977ee75
...@@ -524,12 +524,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -524,12 +524,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
thumbCenterY + CupertinoThumbPainter.radius, thumbCenterY + CupertinoThumbPainter.radius,
); );
_clipRRectLayer = context.pushClipRRect(needsCompositing, Offset.zero, thumbBounds, trackRRect, (PaintingContext innerContext, Offset offset) { _clipRRectLayer.layer = context.pushClipRRect(needsCompositing, Offset.zero, thumbBounds, trackRRect, (PaintingContext innerContext, Offset offset) {
const CupertinoThumbPainter.switchThumb().paint(innerContext.canvas, thumbBounds); const CupertinoThumbPainter.switchThumb().paint(innerContext.canvas, thumbBounds);
}, oldLayer: _clipRRectLayer); }, oldLayer: _clipRRectLayer.layer);
} }
ClipRRectLayer? _clipRRectLayer; final LayerHandle<ClipRRectLayer> _clipRRectLayer = LayerHandle<ClipRRectLayer>();
@override
void dispose() {
_clipRRectLayer.layer = null;
super.dispose();
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder description) { void debugFillProperties(DiagnosticPropertiesBuilder description) {
......
...@@ -289,19 +289,25 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox { ...@@ -289,19 +289,25 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
} }
final BoxParentData childParentData = child!.parentData! as BoxParentData; final BoxParentData childParentData = child!.parentData! as BoxParentData;
_clipPathLayer = context.pushClipPath( _clipPathLayer.layer = context.pushClipPath(
needsCompositing, needsCompositing,
offset + childParentData.offset, offset + childParentData.offset,
Offset.zero & child!.size, Offset.zero & child!.size,
_clipPath(), _clipPath(),
(PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child!, innerOffset), (PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child!, innerOffset),
oldLayer: _clipPathLayer, oldLayer: _clipPathLayer.layer,
); );
} }
ClipPathLayer? _clipPathLayer; final LayerHandle<ClipPathLayer> _clipPathLayer = LayerHandle<ClipPathLayer>();
Paint? _debugPaint; Paint? _debugPaint;
@override
void dispose() {
_clipPathLayer.layer = null;
super.dispose();
}
@override @override
void debugPaintSize(PaintingContext context, Offset offset) { void debugPaintSize(PaintingContext context, Offset offset) {
assert(() { assert(() {
......
...@@ -1502,15 +1502,15 @@ class _RenderDecoration extends RenderBox { ...@@ -1502,15 +1502,15 @@ class _RenderDecoration extends RenderBox {
_labelTransform = Matrix4.identity() _labelTransform = Matrix4.identity()
..translate(dx, labelOffset.dy + dy) ..translate(dx, labelOffset.dy + dy)
..scale(scale); ..scale(scale);
_transformLayer = context.pushTransform( layer = context.pushTransform(
needsCompositing, needsCompositing,
offset, offset,
_labelTransform!, _labelTransform!,
_paintLabel, _paintLabel,
oldLayer: _transformLayer, oldLayer: layer as TransformLayer?,
); );
} else { } else {
_transformLayer = null; layer = null;
} }
doPaint(icon); doPaint(icon);
...@@ -1524,8 +1524,6 @@ class _RenderDecoration extends RenderBox { ...@@ -1524,8 +1524,6 @@ class _RenderDecoration extends RenderBox {
doPaint(counter); doPaint(counter);
} }
TransformLayer? _transformLayer;
@override @override
bool hitTestSelf(Offset position) => true; bool hitTestSelf(Offset position) => true;
......
...@@ -327,19 +327,25 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { ...@@ -327,19 +327,25 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) { if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) {
final Rect rect = Offset.zero & size; final Rect rect = Offset.zero & size;
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
rect, rect,
super.paint, super.paint,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
super.paint(context, offset); super.paint(context, offset);
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
} }
...@@ -301,6 +301,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -301,6 +301,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_foregroundRenderObject = null; _foregroundRenderObject = null;
_backgroundRenderObject?.dispose(); _backgroundRenderObject?.dispose();
_backgroundRenderObject = null; _backgroundRenderObject = null;
_clipRectLayer.layer = null;
_cachedBuiltInForegroundPainters?.dispose(); _cachedBuiltInForegroundPainters?.dispose();
_cachedBuiltInPainters?.dispose(); _cachedBuiltInPainters?.dispose();
super.dispose(); super.dispose();
...@@ -4007,22 +4008,22 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -4007,22 +4008,22 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
_computeTextMetricsIfNeeded(); _computeTextMetricsIfNeeded();
if (_hasVisualOverflow && clipBehavior != Clip.none) { if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
_paintContents, _paintContents,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
_paintContents(context, offset); _paintContents(context, offset);
} }
_paintHandleLayers(context, getEndpointsForSelection(selection!)); _paintHandleLayers(context, getEndpointsForSelection(selection!));
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override @override
Rect? describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null; Rect? describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null;
......
...@@ -1084,17 +1084,17 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl ...@@ -1084,17 +1084,17 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
return; return;
if (clipBehavior == Clip.none) { if (clipBehavior == Clip.none) {
_clipRectLayer = null; _clipRectLayer.layer = null;
defaultPaint(context, offset); defaultPaint(context, offset);
} else { } else {
// We have overflow and the clipBehavior isn't none. Clip it. // We have overflow and the clipBehavior isn't none. Clip it.
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
defaultPaint, defaultPaint,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} }
...@@ -1141,7 +1141,13 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl ...@@ -1141,7 +1141,13 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
}()); }());
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
Rect? describeApproximatePaintClip(RenderObject child) => _hasOverflow ? Offset.zero & size : null; Rect? describeApproximatePaintClip(RenderObject child) => _hasOverflow ? Offset.zero & size : null;
......
...@@ -389,21 +389,27 @@ class RenderFlow extends RenderBox ...@@ -389,21 +389,27 @@ class RenderFlow extends RenderBox
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (clipBehavior == Clip.none) { if (clipBehavior == Clip.none) {
_clipRectLayer = null; _clipRectLayer.layer = null;
_paintWithDelegate(context, offset); _paintWithDelegate(context, offset);
} else { } else {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
_paintWithDelegate, _paintWithDelegate,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
......
...@@ -88,11 +88,146 @@ class AnnotationResult<T> { ...@@ -88,11 +88,146 @@ class AnnotationResult<T> {
/// [SceneBuilder.build] to obtain a [Scene]. A [Scene] can then be painted /// [SceneBuilder.build] to obtain a [Scene]. A [Scene] can then be painted
/// using [dart:ui.FlutterView.render]. /// using [dart:ui.FlutterView.render].
/// ///
/// ## Memory
///
/// Layers retain resources between frames to speed up rendering. A layer will
/// retain these resources until all [LayerHandle]s referring to the layer have
/// nulled out their references.
///
/// Layers must not be used after disposal. If a RenderObject needs to maintain
/// a layer for later usage, it must create a handle to that layer. This is
/// handled automatically for the [RenderObject.layer] property, but additional
/// layers must use their own [LayerHandle].
///
/// {@tool snippet}
///
/// This [RenderObject] is a repaint boundary that pushes an additional
/// [ClipRectLayer].
///
/// ```dart
/// class ClippingRenderObject extends RenderBox {
/// final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
///
/// @override
/// bool get isRepaintBoundary => true; // The [layer] property will be used.
///
/// @override
/// void paint(PaintingContext context, Offset offset) {
/// _clipRectLayer.layer = context.pushClipRect(
/// needsCompositing,
/// offset,
/// Offset.zero & size,
/// super.paint,
/// clipBehavior: Clip.hardEdge,
/// oldLayer: _clipRectLayer.layer,
/// );
/// }
///
/// @override
/// void dispose() {
/// _clipRectLayer.layer = null;
/// super.dispose();
/// }
/// }
/// ```
/// {@end-tool}
/// See also: /// See also:
/// ///
/// * [RenderView.compositeFrame], which implements this recomposition protocol /// * [RenderView.compositeFrame], which implements this recomposition protocol
/// for painting [RenderObject] trees on the display. /// for painting [RenderObject] trees on the display.
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// If asserts are enabled, returns whether [dispose] has
/// been called since the last time any retained resources were created.
///
/// Throws an exception if asserts are disabled.
bool get debugDisposed {
late bool disposed;
assert(() {
disposed = _debugDisposed;
return true;
}());
return disposed;
}
// TODO(dnfield): https://github.com/flutter/flutter/issues/85066
final bool _debugDisposed = false;
/// Set when this layer is appended to a [ContainerLayer], and
/// unset when it is removed.
///
/// This cannot be set from [attach] or [detach] which is called when an
/// entire subtree is attached to or detached from an owner. Layers may be
/// appended to or removed from a [ContainerLayer] regardless of whether they
/// are attached or detached, and detaching a layer from an owner does not
/// imply that it has been removed from its parent.
final LayerHandle<Layer> _parentHandle = LayerHandle<Layer>();
/// Incremeneted by [LayerHandle].
int _refCount = 0;
/// Called by [LayerHandle].
void _unref() {
assert(_refCount > 0);
_refCount -= 1;
if (_refCount == 0) {
dispose();
}
}
/// Returns the number of objects holding a [LayerHandle] to this layer.
///
/// This method throws if asserts are disabled.
int get debugHandleCount {
late int count;
assert(() {
count = _refCount;
return true;
}());
return count;
}
/// Clears any retained resources that this layer holds.
///
/// This method must dispose resources such as [EngineLayer] and [Picture]
/// objects. The layer is still usable after this call, but any graphics
/// related resources it holds will need to be recreated.
///
/// This method _only_ disposes resources for this layer. For example, if it
/// is a [ContainerLayer], it does not dispose resources of any children.
/// However, [ContainerLayer]s do remove any children they have when
/// this method is called, and if this layer was the last holder of a removed
/// child handle, the child may recursively clean up its resources.
///
/// This method automatically gets called when all outstanding [LayerHandle]s
/// are disposed. [LayerHandle] objects are typically held by the [parent]
/// layer of this layer and any [RenderObject]s that participated in creating
/// it.
///
/// After calling this method, the object is unusable.
@mustCallSuper
@protected
@visibleForTesting
void dispose() {
assert(
!_debugDisposed,
'Layers must only be disposed once. This is typically handled by '
'LayerHandle and createHandle. Subclasses should not directly call '
'dispose, except to call super.dispose() in an overridden dispose '
'method. Tests must only call dispose once.',
);
assert(() {
assert(
_refCount == 0,
'Do not directly call dispose on a $runtimeType. Instead, '
'use createHandle and LayerHandle.dispose.',
);
// TODO(dnfield): enable this. https://github.com/flutter/flutter/issues/85066
// _debugDisposed = true;
return true;
}());
_engineLayer?.dispose();
_engineLayer = null;
}
/// This layer's parent in the layer tree. /// This layer's parent in the layer tree.
/// ///
/// The [parent] of the root node in the layer tree is null. /// The [parent] of the root node in the layer tree is null.
...@@ -134,6 +269,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -134,6 +269,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
'$runtimeType with alwaysNeedsAddToScene set called markNeedsAddToScene.\n' '$runtimeType with alwaysNeedsAddToScene set called markNeedsAddToScene.\n'
"The layer's alwaysNeedsAddToScene is set to true, and therefore it should not call markNeedsAddToScene.", "The layer's alwaysNeedsAddToScene is set to true, and therefore it should not call markNeedsAddToScene.",
); );
assert(!_debugDisposed);
// Already marked. Short-circuit. // Already marked. Short-circuit.
if (_needsAddToScene) { if (_needsAddToScene) {
...@@ -187,6 +323,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -187,6 +323,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// layer. The web engine could, for example, update the properties of /// layer. The web engine could, for example, update the properties of
/// previously rendered HTML DOM nodes rather than creating new nodes. /// previously rendered HTML DOM nodes rather than creating new nodes.
@protected @protected
@visibleForTesting
ui.EngineLayer? get engineLayer => _engineLayer; ui.EngineLayer? get engineLayer => _engineLayer;
/// Sets the engine layer used to render this layer. /// Sets the engine layer used to render this layer.
...@@ -195,7 +332,11 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -195,7 +332,11 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// in turn returns the engine layer produced by one of [ui.SceneBuilder]'s /// in turn returns the engine layer produced by one of [ui.SceneBuilder]'s
/// "push" methods, such as [ui.SceneBuilder.pushOpacity]. /// "push" methods, such as [ui.SceneBuilder.pushOpacity].
@protected @protected
@visibleForTesting
set engineLayer(ui.EngineLayer? value) { set engineLayer(ui.EngineLayer? value) {
assert(!_debugDisposed);
_engineLayer?.dispose();
_engineLayer = value; _engineLayer = value;
if (!alwaysNeedsAddToScene) { if (!alwaysNeedsAddToScene) {
// The parent must construct a new engine layer to add this layer to, and // The parent must construct a new engine layer to add this layer to, and
...@@ -415,7 +556,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -415,7 +556,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// ///
/// Defaults to the value of [RenderObject.debugCreator] for the render object /// Defaults to the value of [RenderObject.debugCreator] for the render object
/// that created this layer. Used in debug messages. /// that created this layer. Used in debug messages.
dynamic debugCreator; Object? debugCreator;
@override @override
String toStringShort() => '${super.toStringShort()}${ owner == null ? " DETACHED" : ""}'; String toStringShort() => '${super.toStringShort()}${ owner == null ? " DETACHED" : ""}';
...@@ -424,14 +565,83 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -424,14 +565,83 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Object>('owner', owner, level: parent != null ? DiagnosticLevel.hidden : DiagnosticLevel.info, defaultValue: null)); properties.add(DiagnosticsProperty<Object>('owner', owner, level: parent != null ? DiagnosticLevel.hidden : DiagnosticLevel.info, defaultValue: null));
properties.add(DiagnosticsProperty<dynamic>('creator', debugCreator, defaultValue: null, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<Object?>('creator', debugCreator, defaultValue: null, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<String>('engine layer', describeIdentity(_engineLayer))); if (_engineLayer != null) {
properties.add(DiagnosticsProperty<String>('engine layer', describeIdentity(_engineLayer)));
}
properties.add(DiagnosticsProperty<int>('handles', debugHandleCount));
} }
} }
/// A handle to prevent a [Layer]'s platform graphics resources from being
/// disposed.
///
/// [Layer] objects retain native resourses such as [EngineLayer]s and [Picture]
/// objects. These objects may in turn retain large chunks of texture memory,
/// either directly or indirectly.
///
/// The layer's native resources must be retained as long as there is some
/// object that can add it to a scene. Typically, this is either its
/// [Layer.parent] or an undisposed [RenderObject] that will append it to a
/// [ContainerLayer]. Layers automatically hold a handle to their children, and
/// RenderObjects automatically hold a handle to their [RenderObject.layer] as
/// well as any [PictureLayer]s that they paint into using the
/// [PaintingContext.canvas]. A layer automatically releases its resources once
/// at least one handle has been acquired and all handles have been disposed.
/// [RenderObject]s that create additional layer objects must manually manage
/// the handles for that layer similarly to the implementation of
/// [RenderObject.layer].
///
/// A handle is automatically managed for [RenderObject.layer].
///
/// If a [RenderObject] creates layers in addition to its [RenderObject.layer]
/// and it intends to reuse those layers separately from [RenderObject.layer],
/// it must create a handle to that layer and dispose of it when the layer is
/// no longer needed. For example, if it re-creates or nulls out an existing
/// layer in [RenderObject.paint], it should dispose of the handle to the
/// old layer. It should also dispose of any layer handles it holds in
/// [RenderObject.dispose].
class LayerHandle<T extends Layer> {
/// Create a new layer handle, optionally referencing a [Layer].
LayerHandle([this._layer]) {
if (_layer != null) {
_layer!._refCount += 1;
}
}
T? _layer;
/// The [Layer] whose resources this object keeps alive.
///
/// Setting a new value will or null dispose the previously held layer if
/// there are no other open handles to that layer.
T? get layer => _layer;
set layer(T? layer) {
assert(
layer?.debugDisposed != true,
'Attempted to create a handle to an already disposed layer: $layer.',
);
if (identical(layer, _layer)) {
return;
}
_layer?._unref();
_layer = layer;
if (_layer != null) {
_layer!._refCount += 1;
}
}
@override
String toString() => 'LayerHandle(${_layer != null ? _layer.toString() : 'DISPOSED'})';
}
/// A composited layer containing a [Picture]. /// A composited layer containing a [Picture].
/// ///
/// Picture layers are always leaves in the layer tree. /// Picture layers are always leaves in the layer tree. They are also
/// responsible for disposing of the [Picture] object they hold. This is
/// typically done when their parent and all [RenderObject]s that participated
/// in painting the picture have been disposed.
class PictureLayer extends Layer { class PictureLayer extends Layer {
/// Creates a leaf layer for the layer tree. /// Creates a leaf layer for the layer tree.
PictureLayer(this.canvasBounds); PictureLayer(this.canvasBounds);
...@@ -453,7 +663,9 @@ class PictureLayer extends Layer { ...@@ -453,7 +663,9 @@ class PictureLayer extends Layer {
ui.Picture? get picture => _picture; ui.Picture? get picture => _picture;
ui.Picture? _picture; ui.Picture? _picture;
set picture(ui.Picture? picture) { set picture(ui.Picture? picture) {
assert(!_debugDisposed);
markNeedsAddToScene(); markNeedsAddToScene();
_picture?.dispose();
_picture = picture; _picture = picture;
} }
...@@ -492,6 +704,12 @@ class PictureLayer extends Layer { ...@@ -492,6 +704,12 @@ class PictureLayer extends Layer {
} }
} }
@override
void dispose() {
picture = null; // Will dispose _picture.
super.dispose();
}
@override @override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(picture != null); assert(picture != null);
...@@ -865,6 +1083,12 @@ class ContainerLayer extends Layer { ...@@ -865,6 +1083,12 @@ class ContainerLayer extends Layer {
return addedLayers; return addedLayers;
} }
@override
void dispose() {
removeAllChildren();
super.dispose();
}
@override @override
void updateSubtreeNeedsAddToScene() { void updateSubtreeNeedsAddToScene() {
super.updateSubtreeNeedsAddToScene(); super.updateSubtreeNeedsAddToScene();
...@@ -917,6 +1141,7 @@ class ContainerLayer extends Layer { ...@@ -917,6 +1141,7 @@ class ContainerLayer extends Layer {
assert(!child.attached); assert(!child.attached);
assert(child.nextSibling == null); assert(child.nextSibling == null);
assert(child.previousSibling == null); assert(child.previousSibling == null);
assert(child._parentHandle.layer == null);
assert(() { assert(() {
Layer node = this; Layer node = this;
while (node.parent != null) while (node.parent != null)
...@@ -930,6 +1155,7 @@ class ContainerLayer extends Layer { ...@@ -930,6 +1155,7 @@ class ContainerLayer extends Layer {
lastChild!._nextSibling = child; lastChild!._nextSibling = child;
_lastChild = child; _lastChild = child;
_firstChild ??= child; _firstChild ??= child;
child._parentHandle.layer = child;
assert(child.attached == attached); assert(child.attached == attached);
} }
...@@ -939,6 +1165,7 @@ class ContainerLayer extends Layer { ...@@ -939,6 +1165,7 @@ class ContainerLayer extends Layer {
assert(child.attached == attached); assert(child.attached == attached);
assert(_debugUltimatePreviousSiblingOf(child, equals: firstChild)); assert(_debugUltimatePreviousSiblingOf(child, equals: firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: lastChild)); assert(_debugUltimateNextSiblingOf(child, equals: lastChild));
assert(child._parentHandle.layer != null);
if (child._previousSibling == null) { if (child._previousSibling == null) {
assert(_firstChild == child); assert(_firstChild == child);
_firstChild = child._nextSibling; _firstChild = child._nextSibling;
...@@ -959,6 +1186,7 @@ class ContainerLayer extends Layer { ...@@ -959,6 +1186,7 @@ class ContainerLayer extends Layer {
child._previousSibling = null; child._previousSibling = null;
child._nextSibling = null; child._nextSibling = null;
dropChild(child); dropChild(child);
child._parentHandle.layer = null;
assert(!child.attached); assert(!child.attached);
} }
...@@ -971,6 +1199,8 @@ class ContainerLayer extends Layer { ...@@ -971,6 +1199,8 @@ class ContainerLayer extends Layer {
child._nextSibling = null; child._nextSibling = null;
assert(child.attached == attached); assert(child.attached == attached);
dropChild(child); dropChild(child);
assert(child._parentHandle != null);
child._parentHandle.layer = null;
child = next; child = next;
} }
_firstChild = null; _firstChild = null;
......
...@@ -785,22 +785,28 @@ class RenderListWheelViewport ...@@ -785,22 +785,28 @@ class RenderListWheelViewport
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (childCount > 0) { if (childCount > 0) {
if (_shouldClipAtCurrentOffset() && clipBehavior != Clip.none) { if (_shouldClipAtCurrentOffset() && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
_paintVisibleChildren, _paintVisibleChildren,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
_paintVisibleChildren(context, offset); _paintVisibleChildren(context, offset);
} }
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
/// Paints all children visible in the current viewport. /// Paints all children visible in the current viewport.
void _paintVisibleChildren(PaintingContext context, Offset offset) { void _paintVisibleChildren(PaintingContext context, Offset offset) {
......
...@@ -117,30 +117,32 @@ class PaintingContext extends ClipContext { ...@@ -117,30 +117,32 @@ class PaintingContext extends ClipContext {
); );
return true; return true;
}()); }());
OffsetLayer? childLayer = child._layer as OffsetLayer?; OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;
if (childLayer == null) { if (childLayer == null) {
assert(debugAlsoPaintedParent); assert(debugAlsoPaintedParent);
assert(child._layerHandle.layer == null);
// Not using the `layer` setter because the setter asserts that we not // Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not // replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a // apply here because this is exactly the place designed to create a
// layer for repaint boundaries. // layer for repaint boundaries.
child._layer = childLayer = OffsetLayer(); final OffsetLayer layer = OffsetLayer();
child._layerHandle.layer = childLayer = layer;
} else { } else {
assert(debugAlsoPaintedParent || childLayer.attached); assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren(); childLayer.removeAllChildren();
} }
assert(identical(childLayer, child._layer)); assert(identical(childLayer, child._layerHandle.layer));
assert(child._layer is OffsetLayer); assert(child._layerHandle.layer is OffsetLayer);
assert(() { assert(() {
child._layer!.debugCreator = child.debugCreator ?? child.runtimeType; childLayer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true; return true;
}()); }());
childContext ??= PaintingContext(child._layer!, child.paintBounds); childContext ??= PaintingContext(childLayer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero); child._paintWithContext(childContext, Offset.zero);
// Double-check that the paint method did not replace the layer (the first // Double-check that the paint method did not replace the layer (the first
// check is done in the [layer] setter itself). // check is done in the [layer] setter itself).
assert(identical(childLayer, child._layer)); assert(identical(childLayer, child._layerHandle.layer));
childContext.stopRecordingIfNeeded(); childContext.stopRecordingIfNeeded();
} }
...@@ -209,14 +211,14 @@ class PaintingContext extends ClipContext { ...@@ -209,14 +211,14 @@ class PaintingContext extends ClipContext {
includedParent: true, includedParent: true,
includedChild: false, includedChild: false,
); );
child._layer!.debugCreator = child.debugCreator ?? child; child._layerHandle.layer!.debugCreator = child.debugCreator ?? child;
return true; return true;
}()); }());
} }
assert(child._layer is OffsetLayer); assert(child._layerHandle.layer is OffsetLayer);
final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer; final OffsetLayer childOffsetLayer = child._layerHandle.layer! as OffsetLayer;
childOffsetLayer.offset = offset; childOffsetLayer.offset = offset;
appendLayer(child._layer!); appendLayer(childOffsetLayer);
} }
/// Adds a layer to the recording requiring that the recording is already /// Adds a layer to the recording requiring that the recording is already
...@@ -266,6 +268,7 @@ class PaintingContext extends ClipContext { ...@@ -266,6 +268,7 @@ class PaintingContext extends ClipContext {
Canvas get canvas { Canvas get canvas {
if (_canvas == null) if (_canvas == null)
_startRecording(); _startRecording();
assert(_currentLayer != null);
return _canvas!; return _canvas!;
} }
...@@ -391,6 +394,7 @@ class PaintingContext extends ClipContext { ...@@ -391,6 +394,7 @@ class PaintingContext extends ClipContext {
stopRecordingIfNeeded(); stopRecordingIfNeeded();
appendLayer(childLayer); appendLayer(childLayer);
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds); final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset); painter(childContext, offset);
childContext.stopRecordingIfNeeded(); childContext.stopRecordingIfNeeded();
} }
...@@ -969,9 +973,9 @@ class PipelineOwner { ...@@ -969,9 +973,9 @@ class PipelineOwner {
_nodesNeedingPaint = <RenderObject>[]; _nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first). // Sort the dirty nodes in reverse order (deepest first).
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) { for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layer != null); assert(node._layerHandle.layer != null);
if (node._needsPaint && node.owner == this) { if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) { if (node._layerHandle.layer!.attached) {
PaintingContext.repaintCompositedChild(node); PaintingContext.repaintCompositedChild(node);
} else { } else {
node._skippedPaintingOnLayer(); node._skippedPaintingOnLayer();
...@@ -1274,7 +1278,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1274,7 +1278,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@mustCallSuper @mustCallSuper
void dispose() { void dispose() {
assert(!_debugDisposed); assert(!_debugDisposed);
_layer = null; _layerHandle.layer = null;
assert(() { assert(() {
// TODO(dnfield): Enable this assert once clients have had a chance to // TODO(dnfield): Enable this assert once clients have had a chance to
// migrate. // migrate.
...@@ -1478,7 +1482,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1478,7 +1482,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
_needsCompositingBitsUpdate = false; _needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate(); markNeedsCompositingBitsUpdate();
} }
if (_needsPaint && _layer != null) { if (_needsPaint && _layerHandle.layer != null) {
// Don't enter this block if we've never painted at all; // Don't enter this block if we've never painted at all;
// scheduleInitialPaint() will handle it // scheduleInitialPaint() will handle it
_needsPaint = false; _needsPaint = false;
...@@ -2050,14 +2054,22 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2050,14 +2054,22 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// never populating this field, or by setting it to null when the value of /// never populating this field, or by setting it to null when the value of
/// [needsCompositing] changes from true to false. /// [needsCompositing] changes from true to false.
/// ///
/// If a new layer is created and stored in some other field on the render
/// object, the render object must use a [LayerHandle] to store it. A layer
/// handle will prevent the layer from being disposed before the render
/// object is finished with it, and it will also make sure that the layer
/// gets appropriately disposed when the render object creates a replacement
/// or nulls it out. The render object must null out the [LayerHandle.layer]
/// in its [dispose] method.
///
/// If this render object is a repaint boundary, the framework automatically /// If this render object is a repaint boundary, the framework automatically
/// creates an [OffsetLayer] and populates this field prior to calling the /// creates an [OffsetLayer] and populates this field prior to calling the
/// [paint] method. The [paint] method must not replace the value of this /// [paint] method. The [paint] method must not replace the value of this
/// field. /// field.
@protected @protected
ContainerLayer? get layer { ContainerLayer? get layer {
assert(!isRepaintBoundary || (_layer == null || _layer is OffsetLayer)); assert(!isRepaintBoundary || _layerHandle.layer == null || _layerHandle.layer is OffsetLayer);
return _layer; return _layerHandle.layer;
} }
@protected @protected
...@@ -2068,9 +2080,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2068,9 +2080,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
'The framework creates and assigns an OffsetLayer to a repaint ' 'The framework creates and assigns an OffsetLayer to a repaint '
'boundary automatically.', 'boundary automatically.',
); );
_layer = newLayer; _layerHandle.layer = newLayer;
} }
ContainerLayer? _layer;
final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();
/// In debug mode, the compositing layer that this render object uses to repaint. /// In debug mode, the compositing layer that this render object uses to repaint.
/// ///
...@@ -2082,7 +2095,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2082,7 +2095,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
ContainerLayer? get debugLayer { ContainerLayer? get debugLayer {
ContainerLayer? result; ContainerLayer? result;
assert(() { assert(() {
result = _layer; result = _layerHandle.layer;
return true; return true;
}()); }());
return result; return result;
...@@ -2218,7 +2231,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2218,7 +2231,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}()); }());
// If we always have our own layer, then we can just repaint // If we always have our own layer, then we can just repaint
// ourselves without involving any other nodes. // ourselves without involving any other nodes.
assert(_layer is OffsetLayer); assert(_layerHandle.layer is OffsetLayer);
if (owner != null) { if (owner != null) {
owner!._nodesNeedingPaint.add(this); owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate(); owner!.requestVisualUpdate();
...@@ -2251,14 +2264,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2251,14 +2264,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(attached); assert(attached);
assert(isRepaintBoundary); assert(isRepaintBoundary);
assert(_needsPaint); assert(_needsPaint);
assert(_layer != null); assert(_layerHandle.layer != null);
assert(!_layer!.attached); assert(!_layerHandle.layer!.attached);
AbstractNode? node = parent; AbstractNode? node = parent;
while (node is RenderObject) { while (node is RenderObject) {
if (node.isRepaintBoundary) { if (node.isRepaintBoundary) {
if (node._layer == null) if (node._layerHandle.layer == null)
break; // looks like the subtree here has never been painted. let it handle itself. break; // looks like the subtree here has never been painted. let it handle itself.
if (node._layer!.attached) if (node._layerHandle.layer!.attached)
break; // it's the one that detached us, so it's the one that will decide to repaint us. break; // it's the one that detached us, so it's the one that will decide to repaint us.
node._needsPaint = true; node._needsPaint = true;
} }
...@@ -2278,8 +2291,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2278,8 +2291,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(parent is! RenderObject); assert(parent is! RenderObject);
assert(!owner!._debugDoingPaint); assert(!owner!._debugDoingPaint);
assert(isRepaintBoundary); assert(isRepaintBoundary);
assert(_layer == null); assert(_layerHandle.layer == null);
_layer = rootLayer; _layerHandle.layer = rootLayer;
assert(_needsPaint); assert(_needsPaint);
owner!._nodesNeedingPaint.add(this); owner!._nodesNeedingPaint.add(this);
} }
...@@ -2296,9 +2309,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2296,9 +2309,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(parent is! RenderObject); assert(parent is! RenderObject);
assert(!owner!._debugDoingPaint); assert(!owner!._debugDoingPaint);
assert(isRepaintBoundary); assert(isRepaintBoundary);
assert(_layer != null); // use scheduleInitialPaint the first time assert(_layerHandle.layer != null); // use scheduleInitialPaint the first time
_layer!.detach(); _layerHandle.layer!.detach();
_layer = rootLayer; _layerHandle.layer = rootLayer;
markNeedsPaint(); markNeedsPaint();
} }
...@@ -2388,7 +2401,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2388,7 +2401,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
_debugDoingThisPaint = true; _debugDoingThisPaint = true;
debugLastActivePaint = _debugActivePaint; debugLastActivePaint = _debugActivePaint;
_debugActivePaint = this; _debugActivePaint = this;
assert(!isRepaintBoundary || _layer != null); assert(!isRepaintBoundary || _layerHandle.layer != null);
return true; return true;
}()); }());
_needsPaint = false; _needsPaint = false;
...@@ -2983,7 +2996,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2983,7 +2996,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
properties.add(DiagnosticsProperty<ParentData>('parentData', parentData, tooltip: _debugCanParentUseSize == true ? 'can use size' : null, missingIfNull: true)); properties.add(DiagnosticsProperty<ParentData>('parentData', parentData, tooltip: _debugCanParentUseSize == true ? 'can use size' : null, missingIfNull: true));
properties.add(DiagnosticsProperty<Constraints>('constraints', _constraints, missingIfNull: true)); properties.add(DiagnosticsProperty<Constraints>('constraints', _constraints, missingIfNull: true));
// don't access it via the "layer" getter since that's only valid when we don't need paint // don't access it via the "layer" getter since that's only valid when we don't need paint
properties.add(DiagnosticsProperty<ContainerLayer>('layer', _layer, defaultValue: null)); properties.add(DiagnosticsProperty<ContainerLayer>('layer', _layerHandle.layer, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null)); properties.add(DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null));
properties.add(FlagProperty( properties.add(FlagProperty(
'isBlockingSemanticsOfPreviouslyPaintedNodes', 'isBlockingSemanticsOfPreviouslyPaintedNodes',
......
...@@ -209,21 +209,27 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { ...@@ -209,21 +209,27 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
// Clip the texture if it's going to paint out of the bounds of the renter box // Clip the texture if it's going to paint out of the bounds of the renter box
// (see comment in _paintTexture for an explanation of when this happens). // (see comment in _paintTexture for an explanation of when this happens).
if ((size.width < _currentAndroidViewSize.width || size.height < _currentAndroidViewSize.height) && clipBehavior != Clip.none) { if ((size.width < _currentAndroidViewSize.width || size.height < _currentAndroidViewSize.height) && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
true, true,
offset, offset,
offset & size, offset & size,
_paintTexture, _paintTexture,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
return; return;
} }
_clipRectLayer = null; _clipRectLayer.layer = null;
_paintTexture(context, offset); _paintTexture(context, offset);
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
void _paintTexture(PaintingContext context, Offset offset) { void _paintTexture(PaintingContext context, Offset offset) {
// As resizing the Android view happens asynchronously we don't know exactly when is a // As resizing the Android view happens asynchronously we don't know exactly when is a
......
...@@ -117,19 +117,25 @@ class RenderRotatedBox extends RenderBox with RenderObjectWithChildMixin<RenderB ...@@ -117,19 +117,25 @@ class RenderRotatedBox extends RenderBox with RenderObjectWithChildMixin<RenderB
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
_transformLayer = context.pushTransform( _transformLayer.layer = context.pushTransform(
needsCompositing, needsCompositing,
offset, offset,
_paintTransform!, _paintTransform!,
_paintChild, _paintChild,
oldLayer: _transformLayer, oldLayer: _transformLayer.layer,
); );
} else { } else {
_transformLayer = null; _transformLayer.layer = null;
} }
} }
TransformLayer? _transformLayer; final LayerHandle<TransformLayer> _transformLayer = LayerHandle<TransformLayer>();
@override
void dispose() {
_transformLayer.layer = null;
super.dispose();
}
@override @override
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
......
...@@ -801,17 +801,17 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO ...@@ -801,17 +801,17 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO
} }
if (clipBehavior == Clip.none) { if (clipBehavior == Clip.none) {
_clipRectLayer = null; _clipRectLayer.layer = null;
super.paint(context, offset); super.paint(context, offset);
} else { } else {
// We have overflow and the clipBehavior isn't none. Clip it. // We have overflow and the clipBehavior isn't none. Clip it.
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
super.paint, super.paint,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer:_clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} }
...@@ -822,7 +822,13 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO ...@@ -822,7 +822,13 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO
}()); }());
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
Rect? describeApproximatePaintClip(RenderObject child) { Rect? describeApproximatePaintClip(RenderObject child) {
......
...@@ -632,21 +632,27 @@ class RenderStack extends RenderBox ...@@ -632,21 +632,27 @@ class RenderStack extends RenderBox
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (clipBehavior != Clip.none && _hasVisualOverflow) { if (clipBehavior != Clip.none && _hasVisualOverflow) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
paintStack, paintStack,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
paintStack(context, offset); paintStack(context, offset);
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
Rect? describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null; Rect? describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null;
......
...@@ -632,21 +632,27 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -632,21 +632,27 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
if (firstChild == null) if (firstChild == null)
return; return;
if (hasVisualOverflow && clipBehavior != Clip.none) { if (hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
_paintContents, _paintContents,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
_paintContents(context, offset); _paintContents(context, offset);
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
for (final RenderSliver child in childrenInPaintOrder) { for (final RenderSliver child in childrenInPaintOrder) {
......
...@@ -761,21 +761,27 @@ class RenderWrap extends RenderBox ...@@ -761,21 +761,27 @@ class RenderWrap extends RenderBox
// TODO(ianh): move the debug flex overflow paint logic somewhere common so // TODO(ianh): move the debug flex overflow paint logic somewhere common so
// it can be reused here // it can be reused here
if (_hasVisualOverflow && clipBehavior != Clip.none) { if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
defaultPaint, defaultPaint,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
defaultPaint(context, offset); defaultPaint(context, offset);
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
......
...@@ -787,21 +787,27 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -787,21 +787,27 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow && clipBehavior != Clip.none) { if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
paintStack, paintStack,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
paintStack(context, offset); paintStack(context, offset);
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
void visitChildrenForSemantics(RenderObjectVisitor visitor) { void visitChildrenForSemantics(RenderObjectVisitor visitor) {
......
...@@ -620,22 +620,28 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -620,22 +620,28 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
} }
if (_shouldClipAtPaintOffset(paintOffset) && clipBehavior != Clip.none) { if (_shouldClipAtPaintOffset(paintOffset) && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
paintContents, paintContents,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer = null; _clipRectLayer.layer = null;
paintContents(context, offset); paintContents(context, offset);
} }
} }
} }
ClipRectLayer? _clipRectLayer; final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override @override
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
......
...@@ -262,6 +262,7 @@ void main() { ...@@ -262,6 +262,7 @@ void main() {
r' owner: RenderView#[0-9a-f]{5}\n' r' owner: RenderView#[0-9a-f]{5}\n'
r' creator: RenderView\n' r' creator: RenderView\n'
r' engine layer: (TransformEngineLayer|PersistedTransform)#[0-9a-f]{5}\n' r' engine layer: (TransformEngineLayer|PersistedTransform)#[0-9a-f]{5}\n'
r' handles: 1\n'
r' offset: Offset\(0\.0, 0\.0\)\n' r' offset: Offset\(0\.0, 0\.0\)\n'
r' transform:\n' r' transform:\n'
r' \[0] 3\.0,0\.0,0\.0,0\.0\n' r' \[0] 3\.0,0\.0,0\.0,0\.0\n'
......
...@@ -648,7 +648,7 @@ void main() { ...@@ -648,7 +648,7 @@ void main() {
child: box200x200, child: box200x200,
); );
layout(defaultBox, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(defaultBox, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultBox.paint(context, Offset.zero); context.paintChild(defaultBox, Offset.zero);
expect(context.clipBehavior, equals(Clip.none)); expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) { for (final Clip clip in Clip.values) {
...@@ -659,7 +659,7 @@ void main() { ...@@ -659,7 +659,7 @@ void main() {
clipBehavior: clip, clipBehavior: clip,
); );
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
box.paint(context, Offset.zero); context.paintChild(box, Offset.zero);
expect(context.clipBehavior, equals(clip)); expect(context.clipBehavior, equals(clip));
} }
}); });
......
...@@ -163,9 +163,10 @@ void main() { ...@@ -163,9 +163,10 @@ void main() {
); );
layout(root); layout(root);
dynamic error; dynamic error;
final PaintingContext context = PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
try { try {
s.debugPaint( s.debugPaint(
PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)), context,
const Offset(0.0, 500), const Offset(0.0, 500),
); );
} catch (e) { } catch (e) {
...@@ -195,9 +196,10 @@ void main() { ...@@ -195,9 +196,10 @@ void main() {
); );
layout(root); layout(root);
dynamic error; dynamic error;
final PaintingContext context = PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
try { try {
s.debugPaint( s.debugPaint(
PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)), context,
const Offset(0.0, 500), const Offset(0.0, 500),
); );
} catch (e) { } catch (e) {
......
...@@ -47,7 +47,7 @@ void main() { ...@@ -47,7 +47,7 @@ void main() {
selection: const TextSelection(baseOffset: 0, extentOffset: 0), selection: const TextSelection(baseOffset: 0, extentOffset: 0),
); );
layout(defaultEditable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(defaultEditable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultEditable.paint(context, Offset.zero); context.paintChild(defaultEditable, Offset.zero);
expect(context.clipBehavior, equals(Clip.hardEdge)); expect(context.clipBehavior, equals(Clip.hardEdge));
context.clipBehavior = Clip.none; // Reset as Clip.none won't write into clipBehavior. context.clipBehavior = Clip.none; // Reset as Clip.none won't write into clipBehavior.
...@@ -63,7 +63,7 @@ void main() { ...@@ -63,7 +63,7 @@ void main() {
clipBehavior: clip, clipBehavior: clip,
); );
layout(editable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(editable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
editable.paint(context, Offset.zero); context.paintChild(editable, Offset.zero);
expect(context.clipBehavior, equals(clip)); expect(context.clipBehavior, equals(clip));
} }
}); });
......
...@@ -62,13 +62,13 @@ void main() { ...@@ -62,13 +62,13 @@ void main() {
// By default, clipBehavior should be Clip.none // By default, clipBehavior should be Clip.none
final RenderFlex defaultFlex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200]); final RenderFlex defaultFlex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200]);
layout(defaultFlex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(defaultFlex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultFlex.paint(context, Offset.zero); context.paintChild(defaultFlex, Offset.zero);
expect(context.clipBehavior, equals(Clip.none)); expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) { for (final Clip clip in Clip.values) {
final RenderFlex flex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200], clipBehavior: clip); final RenderFlex flex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200], clipBehavior: clip);
layout(flex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(flex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
flex.paint(context, Offset.zero); context.paintChild(flex, Offset.zero);
expect(context.clipBehavior, equals(clip)); expect(context.clipBehavior, equals(clip));
} }
}); });
......
...@@ -256,7 +256,7 @@ void main() { ...@@ -256,7 +256,7 @@ void main() {
); );
}); });
test('PictureLayer prints picture, engine layer, and raster cache hints in debug info', () { test('PictureLayer prints picture, raster cache hints in debug info', () {
final PictureRecorder recorder = PictureRecorder(); final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder); final Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint()); canvas.drawPaint(Paint());
...@@ -267,10 +267,20 @@ void main() { ...@@ -267,10 +267,20 @@ void main() {
layer.willChangeHint = false; layer.willChangeHint = false;
final List<String> info = _getDebugInfo(layer); final List<String> info = _getDebugInfo(layer);
expect(info, contains('picture: ${describeIdentity(picture)}')); expect(info, contains('picture: ${describeIdentity(picture)}'));
expect(info, contains('engine layer: ${describeIdentity(null)}')); expect(info, isNot(contains('engine layer: ${describeIdentity(null)}')));
expect(info, contains('raster cache hints: isComplex = true, willChange = false')); expect(info, contains('raster cache hints: isComplex = true, willChange = false'));
}); });
test('Layer prints engineLayer if it is not null in debug info', () {
final ConcreteLayer layer = ConcreteLayer();
List<String> info = _getDebugInfo(layer);
expect(info, isNot(contains('engine layer: ${describeIdentity(null)}')));
layer.engineLayer = FakeEngineLayer();
info = _getDebugInfo(layer);
expect(info, contains('engine layer: ${describeIdentity(layer.engineLayer)}'));
});
test('mutating PictureLayer fields triggers needsAddToScene', () { test('mutating PictureLayer fields triggers needsAddToScene', () {
final PictureLayer pictureLayer = PictureLayer(Rect.zero); final PictureLayer pictureLayer = PictureLayer(Rect.zero);
checkNeedsAddToScene(pictureLayer, () { checkNeedsAddToScene(pictureLayer, () {
...@@ -591,6 +601,135 @@ void main() { ...@@ -591,6 +601,135 @@ void main() {
// layer. // layer.
parent.buildScene(SceneBuilder()); parent.buildScene(SceneBuilder());
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/42767 }, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/42767
// TODO(dnfield): remove this when https://github.com/flutter/flutter/issues/85066 is resolved.
const bool bug85066 = true;
test('PictureLayer does not let you call dispose unless refcount is 0', () {
PictureLayer layer = PictureLayer(Rect.zero);
expect(layer.debugHandleCount, 0);
layer.dispose();
expect(layer.debugDisposed, true, skip: bug85066);
layer = PictureLayer(Rect.zero);
final LayerHandle<PictureLayer> handle = LayerHandle<PictureLayer>(layer);
expect(layer.debugHandleCount, 1);
expect(() => layer.dispose(), throwsAssertionError);
handle.layer = null;
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
expect(() => layer.dispose(), throwsAssertionError, skip: bug85066); // already disposed.
});
test('Layer append/remove increases/decreases handle count', () {
final PictureLayer layer = PictureLayer(Rect.zero);
final ContainerLayer parent = ContainerLayer();
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, false);
parent.append(layer);
expect(layer.debugHandleCount, 1);
expect(layer.debugDisposed, false);
layer.remove();
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
});
test('Layer.dispose disposes the engineLayer', () {
final Layer layer = ConcreteLayer();
final FakeEngineLayer engineLayer = FakeEngineLayer();
layer.engineLayer = engineLayer;
expect(engineLayer.disposed, false);
layer.dispose();
expect(engineLayer.disposed, true);
expect(layer.engineLayer, null);
});
test('Layer.engineLayer (set) disposes the engineLayer', () {
final Layer layer = ConcreteLayer();
final FakeEngineLayer engineLayer = FakeEngineLayer();
layer.engineLayer = engineLayer;
expect(engineLayer.disposed, false);
layer.engineLayer = null;
expect(engineLayer.disposed, true);
});
test('PictureLayer.picture (set) disposes the picture', () {
final PictureLayer layer = PictureLayer(Rect.zero);
final FakePicture picture = FakePicture();
layer.picture = picture;
expect(picture.disposed, false);
layer.picture = null;
expect(picture.disposed, true);
});
test('PictureLayer disposes the picture', () {
final PictureLayer layer = PictureLayer(Rect.zero);
final FakePicture picture = FakePicture();
layer.picture = picture;
expect(picture.disposed, false);
layer.dispose();
expect(picture.disposed, true);
});
test('LayerHandle disposes the layer', () {
final ConcreteLayer layer = ConcreteLayer();
final ConcreteLayer layer2 = ConcreteLayer();
expect(layer.debugHandleCount, 0);
expect(layer2.debugHandleCount, 0);
final LayerHandle<ConcreteLayer> holder = LayerHandle<ConcreteLayer>(layer);
expect(layer.debugHandleCount, 1);
expect(layer.debugDisposed, false);
expect(layer2.debugHandleCount, 0);
expect(layer2.debugDisposed, false);
holder.layer = layer;
expect(layer.debugHandleCount, 1);
expect(layer.debugDisposed, false);
expect(layer2.debugHandleCount, 0);
expect(layer2.debugDisposed, false);
holder.layer = layer2;
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
expect(layer2.debugHandleCount, 1);
expect(layer2.debugDisposed, false);
holder.layer = null;
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
expect(layer2.debugHandleCount, 0);
expect(layer2.debugDisposed, true, skip: bug85066);
expect(() => holder.layer = layer, throwsAssertionError, skip: bug85066);
});
}
class FakeEngineLayer extends Fake implements EngineLayer {
bool disposed = false;
@override
void dispose() {
assert(!disposed);
disposed = true;
}
}
class FakePicture extends Fake implements Picture {
bool disposed = false;
@override
void dispose() {
assert(!disposed);
disposed = true;
}
}
class ConcreteLayer extends Layer {
@override
void addToScene(SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {}
} }
class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer { class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer {
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// TODO(dnfield): remove when https://github.com/flutter/flutter/issues/85066 is resolved.
const bool bug85066 = true;
testWidgets('Tracks picture layers accurately when painting is interleaved with a pushLayer', (WidgetTester tester) async {
// Creates a RenderObject that will paint into multiple picture layers.
// Asserts that both layers get a handle, and that all layers get correctly
// released.
final GlobalKey key = GlobalKey();
await tester.pumpWidget(RepaintBoundary(
child: CustomPaint(
key: key,
painter: SimplePainter(),
child: const RepaintBoundary(child: Placeholder()),
foregroundPainter: SimplePainter(),
),
));
final List<Layer> layers = tester.binding.renderView.debugLayer!.depthFirstIterateChildren();
final RenderObject renderObject = key.currentContext!.findRenderObject()!;
for (final Layer layer in layers) {
expect(layer.debugDisposed, false);
}
await tester.pumpWidget(const SizedBox());
for (final Layer layer in layers) {
expect(layer.debugDisposed, true, skip: bug85066);
}
expect(renderObject.debugDisposed, true);
});
}
class SimplePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawPaint(Paint());
}
@override
bool shouldRepaint(SimplePainter oldDelegate) => true;
}
...@@ -305,7 +305,6 @@ class TestClipPaintingContext extends PaintingContext { ...@@ -305,7 +305,6 @@ class TestClipPaintingContext extends PaintingContext {
ClipRectLayer? oldLayer, ClipRectLayer? oldLayer,
}) { }) {
this.clipBehavior = clipBehavior; this.clipBehavior = clipBehavior;
return null;
} }
Clip clipBehavior = Clip.none; Clip clipBehavior = Clip.none;
......
...@@ -83,7 +83,7 @@ void main() { ...@@ -83,7 +83,7 @@ void main() {
parentData.left = parentData.right = 0; parentData.left = parentData.right = 0;
} }
layout(stack, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(stack, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
stack.paint(context, Offset.zero); context.paintChild(stack, Offset.zero);
expect(context.clipBehavior, equals(clip)); expect(context.clipBehavior, equals(clip));
} }
}); });
......
...@@ -209,13 +209,13 @@ void main() { ...@@ -209,13 +209,13 @@ void main() {
// By default, clipBehavior should be Clip.none // By default, clipBehavior should be Clip.none
final RenderWrap defaultWrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200]); final RenderWrap defaultWrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200]);
layout(defaultWrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(defaultWrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultWrap.paint(context, Offset.zero); context.paintChild(defaultWrap, Offset.zero);
expect(context.clipBehavior, equals(Clip.none)); expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) { for (final Clip clip in Clip.values) {
final RenderWrap wrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200], clipBehavior: clip); final RenderWrap wrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200], clipBehavior: clip);
layout(wrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); layout(wrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
wrap.paint(context, Offset.zero); context.paintChild(wrap, Offset.zero);
expect(context.clipBehavior, equals(clip)); expect(context.clipBehavior, equals(clip));
} }
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment