Browse Source

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

pull/1277/head
Jeremy Koritzinsky 8 years ago
parent
commit
7e174a79e0
  1. 44
      src/Avalonia.Base/Utilities/Ref.cs
  2. 2
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  3. 13
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  4. 37
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  5. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  6. 17
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  7. 23
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  8. 15
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  9. 68
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

44
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;
}
}

2
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();
}

13
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

37
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)

7
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>

17
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
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
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
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(

Loading…
Cancel
Save