diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index f9e8b29b95..c9ebeefebc 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -4,22 +4,58 @@ using System.Threading; namespace Avalonia.Utilities { + /// + /// A ref-counted wrapper for a disposable object. + /// + /// public interface IRef : IDisposable where T : class { + /// + /// The item that is being ref-counted. + /// T Item { get; } + + /// + /// Create another reference to this object and increment the refcount. + /// + /// A new reference to this object. IRef Clone(); + + /// + /// Create another reference to the same object, but cast the object to a different type. + /// + /// The type of the new reference. + /// A reference to the value as the new type but sharing the refcount. IRef CloneAs() where TResult : class; + + + /// + /// The current refcount of the object tracked in this reference. For debugging/unit test use only. + /// + int RefCount { get; } } public static class RefCountable { + /// + /// Create a reference counted object wrapping the given item. + /// + /// The type of item. + /// The item to refcount. + /// The refcounted reference to the item. public static IRef Create(T item) where T : class, IDisposable { return new Ref(item, new RefCounter(item)); } - + + /// + /// Create an non-owning non-clonable reference to an item. + /// + /// The type of item. + /// The item. + /// A temporary reference that cannot be cloned that doesn't own the element. public static IRef CreateUnownedNotClonable(T item) where T : class => new TempRef(item); @@ -40,6 +76,8 @@ namespace Avalonia.Utilities public IRef CloneAs() where TResult : class => throw new NotSupportedException(); + + public int RefCount => 1; } class RefCounter @@ -90,6 +128,8 @@ namespace Avalonia.Utilities old = current; } } + + internal int RefCount => _refs; } class Ref : CriticalFinalizerObject, IRef where T : class @@ -161,6 +201,8 @@ namespace Avalonia.Utilities throw new ObjectDisposedException("Ref<" + typeof(T) + ">"); } } + + public int RefCount => _counter.RefCount; } } diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 892cd935cf..cb98ed9a9c 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -37,7 +37,7 @@ namespace Avalonia.Media.Imaging /// Initializes a new instance of the class. /// /// A platform-specific bitmap implementation. - protected Bitmap(IRef impl) + public Bitmap(IRef impl) { PlatformImpl = impl.Clone(); } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 989e2eacdf..709ec670ac 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -112,7 +112,12 @@ namespace Avalonia.Rendering /// /// Disposes of the renderer and detaches from the render loop. /// - public void Dispose() => Stop(); + public void Dispose() + { + var scene = Interlocked.Exchange(ref _scene, null); + scene.Dispose(); + Stop(); + } /// public IEnumerable HitTest(Point p, IVisual root, Func filter) @@ -391,14 +396,16 @@ namespace Avalonia.Rendering } } - Interlocked.Exchange(ref _scene, scene); + var oldScene = Interlocked.Exchange(ref _scene, scene); + oldScene.Dispose(); _dirty.Clear(); (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size)); } else { - Interlocked.Exchange(ref _scene, null); + var oldScene = Interlocked.Exchange(ref _scene, null); + oldScene.Dispose(); } } finally diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index d6ab4bd5cb..fa57d07e23 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -81,7 +81,7 @@ namespace Avalonia.Rendering.SceneGraph /// public void Dispose() { - _node?.Dispose(); + // Nothing to do here since we allocate no unmanaged resources. } /// @@ -105,7 +105,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) { - Add(RefCountable.Create(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)))); + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); } else { @@ -120,7 +120,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect)) { - Add(RefCountable.Create(new ImageNode(Transform, source, opacity, sourceRect, destRect))); + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect)); } else { @@ -142,7 +142,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) { - Add(RefCountable.Create(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)))); + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); } else { @@ -157,7 +157,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(Transform, null, pen, rect, cornerRadius)) { - Add(RefCountable.Create(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush)))); + Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush))); } else { @@ -172,7 +172,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(Transform, foreground, origin, text)) { - Add(RefCountable.Create(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground)))); + Add(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground))); } else { @@ -187,7 +187,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(Transform, brush, null, rect, cornerRadius)) { - Add(RefCountable.Create(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush)))); + Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush))); } else { @@ -207,7 +207,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(null)) { - Add(RefCountable.Create(new ClipNode())); + Add(new ClipNode()); } else { @@ -222,7 +222,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(null)) { - Add(RefCountable.Create((new GeometryClipNode()))); + Add((new GeometryClipNode())); } else { @@ -237,7 +237,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(null)) { - Add(RefCountable.Create(new OpacityNode())); + Add(new OpacityNode()); } else { @@ -252,7 +252,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(null, null)) { - Add(RefCountable.Create(new OpacityMaskNode())); + Add(new OpacityMaskNode()); } else { @@ -267,7 +267,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(clip)) { - Add(RefCountable.Create(new ClipNode(clip))); + Add(new ClipNode(clip)); } else { @@ -282,7 +282,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(clip)) { - Add(RefCountable.Create(new GeometryClipNode(clip))); + Add(new GeometryClipNode(clip)); } else { @@ -297,7 +297,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(opacity)) { - Add(RefCountable.Create(new OpacityNode(opacity))); + Add(new OpacityNode(opacity)); } else { @@ -312,7 +312,7 @@ namespace Avalonia.Rendering.SceneGraph if (next == null || !next.Item.Equals(mask, bounds)) { - Add(RefCountable.Create(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)))); + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); } else { @@ -356,6 +356,13 @@ namespace Avalonia.Rendering.SceneGraph public int DrawOperationIndex { get; } } + private void Add(IDrawOperation node) + { + var refCounted = RefCountable.Create(node); + Add(refCounted); + refCounted.Dispose(); // Dispose our reference + } + private void Add(IRef node) { if (_drawOperationindex < _node.DrawOperations.Count) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 9216bae8ad..f2e4f5fdbd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a scene graph used by the . /// - public class Scene + public class Scene : IDisposable { private Dictionary _index; @@ -96,6 +96,11 @@ namespace Avalonia.Rendering.SceneGraph return result; } + public void Dispose() + { + Root.Dispose(); + } + /// /// Tries to find a node in the scene graph representing the specified visual. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 41ff802164..b219a74119 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -271,22 +271,21 @@ namespace Avalonia.Rendering.SceneGraph private static void Deindex(Scene scene, VisualNode node) { - scene.Remove(node); - node.SubTreeUpdated = true; - - scene.Layers[node.LayerRoot].Dirty.Add(node.Bounds); - - node.Visual.TransformedBounds = null; - foreach (VisualNode child in node.Children) { - var geometry = child as IDrawOperation; - if (child is VisualNode visual) { Deindex(scene, visual); } } + scene.Remove(node); + + node.SubTreeUpdated = true; + + scene.Layers[node.LayerRoot].Dirty.Add(node.Bounds); + + node.Visual.TransformedBounds = null; + if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) { diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs index 0fa70315fd..6faba372d5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.UnitTests; using Avalonia.Utilities; @@ -192,5 +193,27 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(2, node.DrawOperations.Count); } + + [Fact] + public void Trimmed_DrawOperations_Releases_Reference() + { + var node = new VisualNode(new TestRoot(), null); + var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0)); + var layers = new SceneLayers(node.Visual); + var target = new DeferredDrawingContextImpl(null, layers); + + node.LayerRoot = node.Visual; + node.AddDrawOperation(operation); + Assert.Equal(2, operation.RefCount); + + using (target.BeginUpdate(node)) + { + target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100)); + } + + Assert.Equal(1, node.DrawOperations.Count); + Assert.NotSame(operation, node.DrawOperations.Single()); + Assert.Equal(1, operation.RefCount); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 76fe103c1b..8c905dab2f 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -2,6 +2,8 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Moq; using Xunit; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph @@ -40,6 +42,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds); } + [Fact] + public void Image_Node_Releases_Reference_To_Bitmap_On_Dispose() + { + var bitmap = RefCountable.Create(Mock.Of()); + var imageNode = new ImageNode(Matrix.Identity, bitmap, 1, new Rect(1,1,1,1), new Rect(1,1,1,1)); + + Assert.Equal(2, bitmap.RefCount); + + imageNode.Dispose(); + + Assert.Equal(1, bitmap.RefCount); + } + private class TestDrawOperation : DrawOperation { public TestDrawOperation(Rect bounds, Matrix transform, Pen pen) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 2eb3dfb15c..f44be3f82e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -11,6 +11,8 @@ using Moq; using Avalonia.Platform; using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { @@ -678,6 +680,72 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Disposing_Scene_Releases_DrawOperation_References() + { + using (TestApplication()) + { + var bitmap = RefCountable.Create(Mock.Of()); + Image img; + var tree = new TestRoot + { + Child = img = new Image + { + Source = new Bitmap(bitmap) + } + }; + + Assert.Equal(2, bitmap.RefCount); + IRef operation; + + using (var scene = new Scene(tree)) + { + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + operation = scene.FindNode(img).DrawOperations[0]; + Assert.Equal(1, operation.RefCount); + + Assert.Equal(3, bitmap.RefCount); + } + Assert.Equal(0, operation.RefCount); + Assert.Equal(2, bitmap.RefCount); + } + } + + [Fact] + public void Replacing_Control_Releases_DrawOperation_Reference() + { + using (TestApplication()) + { + var bitmap = RefCountable.Create(Mock.Of()); + Image img; + var tree = new TestRoot + { + Child = img = new Image + { + Source = new Bitmap(bitmap) + } + }; + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + var operation = scene.FindNode(img).DrawOperations[0]; + + tree.Child = new Decorator(); + + using (var result = scene.Clone()) + { + sceneBuilder.Update(result, img); + scene.Dispose(); + + Assert.Equal(0, operation.RefCount); + Assert.Equal(2, bitmap.RefCount); + } + } + } + private IDisposable TestApplication() { return UnitTestApplication.Start(