瀏覽代碼

Refactored refcounting and added tests for refcount tracking in the DeferredRenderer and friends.

Jeremy Koritzinsky 7 年之前
父節點
當前提交
7e174a79e0

+ 43 - 1
src/Avalonia.Base/Utilities/Ref.cs

@@ -4,22 +4,58 @@ using System.Threading;
 
 namespace Avalonia.Utilities
 {
+    /// <summary>
+    /// A ref-counted wrapper for a disposable object.
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
     public interface IRef<out T> : IDisposable where T : class
     {
+        /// <summary>
+        /// The item that is being ref-counted.
+        /// </summary>
         T Item { get; }
+
+        /// <summary>
+        /// Create another reference to this object and increment the refcount.
+        /// </summary>
+        /// <returns>A new reference to this object.</returns>
         IRef<T> Clone();
+
+        /// <summary>
+        /// Create another reference to the same object, but cast the object to a different type.
+        /// </summary>
+        /// <typeparam name="TResult">The type of the new reference.</typeparam>
+        /// <returns>A reference to the value as the new type but sharing the refcount.</returns>
         IRef<TResult> CloneAs<TResult>() where TResult : class;
+
+
+        /// <summary>
+        /// The current refcount of the object tracked in this reference. For debugging/unit test use only.
+        /// </summary>
+        int RefCount { get; }
     }
 
     
 
     public static class RefCountable
     {
+        /// <summary>
+        /// Create a reference counted object wrapping the given item.
+        /// </summary>
+        /// <typeparam name="T">The type of item.</typeparam>
+        /// <param name="item">The item to refcount.</param>
+        /// <returns>The refcounted reference to the item.</returns>
         public static IRef<T> Create<T>(T item) where T : class, IDisposable
         {
             return new Ref<T>(item, new RefCounter(item));
         }
-
+        
+        /// <summary>
+        /// Create an non-owning non-clonable reference to an item.
+        /// </summary>
+        /// <typeparam name="T">The type of item.</typeparam>
+        /// <param name="item">The item.</param>
+        /// <returns>A temporary reference that cannot be cloned that doesn't own the element.</returns>
         public static IRef<T> CreateUnownedNotClonable<T>(T item) where T : class
             => new TempRef<T>(item);
 
@@ -40,6 +76,8 @@ namespace Avalonia.Utilities
 
             public IRef<TResult> CloneAs<TResult>() 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<T> : CriticalFinalizerObject, IRef<T> where T : class
@@ -161,6 +201,8 @@ namespace Avalonia.Utilities
                     throw new ObjectDisposedException("Ref<" + typeof(T) + ">");
                 }
             }
+
+            public int RefCount => _counter.RefCount;
         }
     }
 

+ 1 - 1
src/Avalonia.Visuals/Media/Imaging/Bitmap.cs

@@ -37,7 +37,7 @@ namespace Avalonia.Media.Imaging
         /// Initializes a new instance of the <see cref="Bitmap"/> class.
         /// </summary>
         /// <param name="impl">A platform-specific bitmap implementation.</param>
-        protected Bitmap(IRef<IBitmapImpl> impl)
+        public Bitmap(IRef<IBitmapImpl> impl)
         {
             PlatformImpl = impl.Clone();
         }

+ 10 - 3
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -112,7 +112,12 @@ namespace Avalonia.Rendering
         /// <summary>
         /// Disposes of the renderer and detaches from the render loop.
         /// </summary>
-        public void Dispose() => Stop();
+        public void Dispose()
+        {
+            var scene = Interlocked.Exchange(ref _scene, null);
+            scene.Dispose();
+            Stop();
+        }
 
         /// <inheritdoc/>
         public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> 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

+ 22 - 15
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -81,7 +81,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public void Dispose()
         {
-            _node?.Dispose();
+            // Nothing to do here since we allocate no unmanaged resources.
         }
 
         /// <summary>
@@ -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<IDrawOperation> node)
         {
             if (_drawOperationindex < _node.DrawOperations.Count)

+ 6 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// Represents a scene graph used by the <see cref="DeferredRenderer"/>.
     /// </summary>
-    public class Scene
+    public class Scene : IDisposable
     {
         private Dictionary<IVisual, IVisualNode> _index;
 
@@ -96,6 +96,11 @@ namespace Avalonia.Rendering.SceneGraph
             return result;
         }
 
+        public void Dispose()
+        {
+            Root.Dispose();
+        }
+
         /// <summary>
         /// Tries to find a node in the scene graph representing the specified visual.
         /// </summary>

+ 8 - 9
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)
             {

+ 23 - 0
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);
+        }
     }
 }

+ 15 - 0
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<IBitmapImpl>());
+            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)

+ 68 - 0
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<IBitmapImpl>());
+                Image img;
+                var tree = new TestRoot
+                {
+                    Child = img = new Image
+                    {
+                        Source = new Bitmap(bitmap)
+                    }
+                };
+
+                Assert.Equal(2, bitmap.RefCount);
+                IRef<IDrawOperation> 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<IBitmapImpl>());
+                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(