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(