diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs new file mode 100644 index 0000000000..c9ebeefebc --- /dev/null +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -0,0 +1,209 @@ +using System; +using System.Runtime.ConstrainedExecution; +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); + + class TempRef : IRef where T : class + { + public void Dispose() + { + + } + + public TempRef(T item) + { + Item = item; + } + + public T Item { get; } + public IRef Clone() => throw new NotSupportedException(); + + public IRef CloneAs() where TResult : class + => throw new NotSupportedException(); + + public int RefCount => 1; + } + + class RefCounter + { + private IDisposable _item; + private volatile int _refs; + + public RefCounter(IDisposable item) + { + _item = item; + _refs = 1; + } + + public void AddRef() + { + var old = _refs; + while (true) + { + if (old == 0) + { + throw new ObjectDisposedException("Cannot add a reference to a nonreferenced item"); + } + var current = Interlocked.CompareExchange(ref _refs, old + 1, old); + if (current == old) + { + break; + } + old = current; + } + } + + public void Release() + { + var old = _refs; + while (true) + { + var current = Interlocked.CompareExchange(ref _refs, old - 1, old); + + if (current == old) + { + if (old == 1) + { + _item.Dispose(); + _item = null; + } + break; + } + old = current; + } + } + + internal int RefCount => _refs; + } + + class Ref : CriticalFinalizerObject, IRef where T : class + { + private T _item; + private RefCounter _counter; + private object _lock = new object(); + + public Ref(T item, RefCounter counter) + { + _item = item; + _counter = counter; + } + + public void Dispose() + { + lock (_lock) + { + if (_item != null) + { + _counter.Release(); + _item = null; + } + GC.SuppressFinalize(this); + } + } + + ~Ref() + { + _counter?.Release(); + } + + public T Item + { + get + { + lock (_lock) + { + return _item; + } + } + } + + public IRef Clone() + { + lock (_lock) + { + if (_item != null) + { + var newRef = new Ref(_item, _counter); + _counter.AddRef(); + return newRef; + } + throw new ObjectDisposedException("Ref<" + typeof(T) + ">"); + } + } + + public IRef CloneAs() where TResult : class + { + lock (_lock) + { + if (_item != null) + { + var castRef = new Ref((TResult)(object)_item, _counter); + Interlocked.MemoryBarrier(); + _counter.AddRef(); + return castRef; + } + throw new ObjectDisposedException("Ref<" + typeof(T) + ">"); + } + } + + public int RefCount => _counter.RefCount; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Controls/WindowIcon.cs b/src/Avalonia.Controls/WindowIcon.cs index dff84ff6ef..26195e6d76 100644 --- a/src/Avalonia.Controls/WindowIcon.cs +++ b/src/Avalonia.Controls/WindowIcon.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls { public WindowIcon(IBitmap bitmap) { - PlatformImpl = AvaloniaLocator.Current.GetService().LoadIcon(bitmap.PlatformImpl); + PlatformImpl = AvaloniaLocator.Current.GetService().LoadIcon(bitmap.PlatformImpl.Item); } public WindowIcon(string fileName) diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index c9f7e0f7ac..cb98ed9a9c 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -4,6 +4,7 @@ using System; using System.IO; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media.Imaging { @@ -19,7 +20,7 @@ namespace Avalonia.Media.Imaging public Bitmap(string fileName) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.LoadBitmap(fileName); + PlatformImpl = RefCountable.Create(factory.LoadBitmap(fileName)); } /// @@ -29,18 +30,33 @@ namespace Avalonia.Media.Imaging public Bitmap(Stream stream) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.LoadBitmap(stream); + PlatformImpl = RefCountable.Create(factory.LoadBitmap(stream)); } /// /// Initializes a new instance of the class. /// /// A platform-specific bitmap implementation. + public Bitmap(IRef impl) + { + PlatformImpl = impl.Clone(); + } + + /// + /// Initializes a new instance of the class. + /// + /// A platform-specific bitmap implementation. Bitmap class takes the ownership. protected Bitmap(IBitmapImpl impl) { - PlatformImpl = impl; + PlatformImpl = RefCountable.Create(impl); } - + + /// + public virtual void Dispose() + { + PlatformImpl.Dispose(); + } + /// /// Initializes a new instance of the class. /// @@ -51,27 +67,24 @@ namespace Avalonia.Media.Imaging /// Bytes per row public Bitmap(PixelFormat format, IntPtr data, int width, int height, int stride) { - PlatformImpl = AvaloniaLocator.Current.GetService() - .LoadBitmap(format, data, width, height, stride); + PlatformImpl = RefCountable.Create(AvaloniaLocator.Current.GetService() + .LoadBitmap(format, data, width, height, stride)); } /// /// Gets the width of the bitmap, in pixels. /// - public int PixelWidth => PlatformImpl.PixelWidth; + public int PixelWidth => PlatformImpl.Item.PixelWidth; /// /// Gets the height of the bitmap, in pixels. /// - public int PixelHeight => PlatformImpl.PixelHeight; + public int PixelHeight => PlatformImpl.Item.PixelHeight; /// /// Gets the platform-specific bitmap implementation. /// - public IBitmapImpl PlatformImpl - { - get; - } + public IRef PlatformImpl { get; } /// /// Saves the bitmap to a file. @@ -79,12 +92,12 @@ namespace Avalonia.Media.Imaging /// The filename. public void Save(string fileName) { - PlatformImpl.Save(fileName); + PlatformImpl.Item.Save(fileName); } public void Save(Stream stream) { - PlatformImpl.Save(stream); + PlatformImpl.Item.Save(stream); } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs index 5a10145d84..465173059e 100644 --- a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs @@ -1,15 +1,17 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.IO; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media.Imaging { /// /// Represents a bitmap image. /// - public interface IBitmap + public interface IBitmap : IDisposable { /// /// Gets the width of the bitmap, in pixels. @@ -24,7 +26,7 @@ namespace Avalonia.Media.Imaging /// /// Gets the platform-specific bitmap implementation. /// - IBitmapImpl PlatformImpl { get; } + IRef PlatformImpl { get; } /// /// Saves the bitmap to a file. diff --git a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs index 9d3f15f69a..52bd8a3c04 100644 --- a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.CompilerServices; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Media.Imaging @@ -21,22 +23,19 @@ namespace Avalonia.Media.Imaging /// The horizontal DPI of the bitmap. /// The vertical DPI of the bitmap. public RenderTargetBitmap(int pixelWidth, int pixelHeight, double dpiX = 96, double dpiY = 96) - : base(CreateImpl(pixelWidth, pixelHeight, dpiX, dpiY)) + : this(RefCountable.Create(CreateImpl(pixelWidth, pixelHeight, dpiX, dpiY))) { } - /// - /// Gets the platform-specific bitmap implementation. - /// - public new IRenderTargetBitmapImpl PlatformImpl => (IRenderTargetBitmapImpl)base.PlatformImpl; + private RenderTargetBitmap(IRef impl) : base(impl) + { + PlatformImpl = impl; + } /// - /// Disposes of the bitmap. + /// Gets the platform-specific bitmap implementation. /// - public void Dispose() - { - PlatformImpl.Dispose(); - } + public new IRef PlatformImpl { get; } /// /// Renders a visual to the . @@ -52,13 +51,13 @@ namespace Avalonia.Media.Imaging /// The horizontal DPI of the bitmap. /// The vertical DPI of the bitmap. /// The platform-specific implementation. - private static IBitmapImpl CreateImpl(int width, int height, double dpiX, double dpiY) + private static IRenderTargetBitmapImpl CreateImpl(int width, int height, double dpiX, double dpiY) { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); return factory.CreateRenderTargetBitmap(width, height, dpiX, dpiY); } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer vbr) => PlatformImpl.CreateDrawingContext(vbr); + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); } } diff --git a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs index 5c5b516ddd..df3e71dc85 100644 --- a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media.Imaging { @@ -16,7 +17,7 @@ namespace Avalonia.Media.Imaging : base(AvaloniaLocator.Current.GetService().CreateWritableBitmap(width, height, format)) { } - - public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl).Lock(); + + public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl.Item).Lock(); } } diff --git a/src/Avalonia.Visuals/Platform/IBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IBitmapImpl.cs index 12ac28e880..d60838452e 100644 --- a/src/Avalonia.Visuals/Platform/IBitmapImpl.cs +++ b/src/Avalonia.Visuals/Platform/IBitmapImpl.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.IO; namespace Avalonia.Platform @@ -8,7 +9,7 @@ namespace Avalonia.Platform /// /// Defines the platform-specific interface for a . /// - public interface IBitmapImpl + public interface IBitmapImpl : IDisposable { /// /// Gets the width of the bitmap, in pixels. diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 3db4527bfb..04bbe713f2 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Platform { @@ -29,7 +30,7 @@ namespace Avalonia.Platform /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect); + void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect); /// /// Draws a bitmap image. @@ -38,7 +39,7 @@ namespace Avalonia.Platform /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); /// /// Draws a line. diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 82cc0a260d..344f3f8f2b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -12,6 +12,7 @@ using System.IO; using Avalonia.Media.Immutable; using System.Threading; using System.Linq; +using Avalonia.Utilities; namespace Avalonia.Rendering { @@ -29,12 +30,12 @@ namespace Avalonia.Rendering private bool _running; private Scene _scene; private DirtyVisuals _dirty; - private IRenderTargetBitmapImpl _overlay; + private IRef _overlay; private bool _updateQueued; private object _rendering = new object(); private int _lastSceneId = -1; private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); - private IDrawOperation _currentDraw; + private IRef _currentDraw; /// /// Initializes a new instance of the class. @@ -111,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) @@ -158,13 +164,13 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; if (childScene != null) { @@ -252,7 +258,7 @@ namespace Avalonia.Rendering foreach (var operation in node.DrawOperations) { _currentDraw = operation; - operation.Render(context); + operation.Item.Render(context); _currentDraw = null; } @@ -277,7 +283,7 @@ namespace Avalonia.Rendering if (node != null) { - using (var context = renderTarget.CreateDrawingContext(this)) + using (var context = renderTarget.Item.CreateDrawingContext(this)) { foreach (var rect in layer.Dirty) { @@ -304,7 +310,7 @@ namespace Avalonia.Rendering { var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling); - using (var context = overlay.CreateDrawingContext(this)) + using (var context = overlay.Item.CreateDrawingContext(this)) { context.Clear(Colors.Transparent); RenderDirtyRects(context); @@ -333,7 +339,7 @@ namespace Avalonia.Rendering foreach (var layer in scene.Layers) { var bitmap = Layers[layer.LayerRoot].Bitmap; - var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); + var sourceRect = new Rect(0, 0, bitmap.Item.PixelWidth, bitmap.Item.PixelHeight); if (layer.GeometryClip != null) { @@ -357,7 +363,7 @@ namespace Avalonia.Rendering if (_overlay != null) { - var sourceRect = new Rect(0, 0, _overlay.PixelWidth, _overlay.PixelHeight); + var sourceRect = new Rect(0, 0, _overlay.Item.PixelWidth, _overlay.Item.PixelHeight); context.DrawImage(_overlay, 0.5, sourceRect, clientRect); } @@ -390,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 @@ -430,7 +438,7 @@ namespace Avalonia.Rendering } } - private IRenderTargetBitmapImpl GetOverlay( + private IRef GetOverlay( IDrawingContextImpl parentContext, Size size, double scaling) @@ -438,11 +446,11 @@ namespace Avalonia.Rendering var pixelSize = size * scaling; if (_overlay == null || - _overlay.PixelWidth != pixelSize.Width || - _overlay.PixelHeight != pixelSize.Height) + _overlay.Item.PixelWidth != pixelSize.Width || + _overlay.Item.PixelHeight != pixelSize.Height) { _overlay?.Dispose(); - _overlay = parentContext.CreateLayer(size); + _overlay = RefCountable.Create(parentContext.CreateLayer(size)); } return _overlay; @@ -455,7 +463,7 @@ namespace Avalonia.Rendering foreach (var layer in Layers) { var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png"); - layer.Bitmap.Save(fileName); + layer.Bitmap.Item.Save(fileName); } } } diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Visuals/Rendering/RenderLayer.cs index ed33295db6..d1b1f1440a 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayer.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Rendering @@ -16,13 +17,13 @@ namespace Avalonia.Rendering IVisual layerRoot) { _drawingContext = drawingContext; - Bitmap = drawingContext.CreateLayer(size); + Bitmap = RefCountable.Create(drawingContext.CreateLayer(size)); Size = size; Scaling = scaling; LayerRoot = layerRoot; } - public IRenderTargetBitmapImpl Bitmap { get; private set; } + public IRef Bitmap { get; private set; } public double Scaling { get; private set; } public Size Size { get; private set; } public IVisual LayerRoot { get; } @@ -31,9 +32,9 @@ namespace Avalonia.Rendering { if (Size != size || Scaling != scaling) { - var resized = _drawingContext.CreateLayer(size); + var resized = RefCountable.Create(_drawingContext.CreateLayer(size)); - using (var context = resized.CreateDrawingContext(null)) + using (var context = resized.Item.CreateDrawingContext(null)) { context.Clear(Colors.Transparent); context.DrawImage(Bitmap, 1, new Rect(Size), new Rect(Size)); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs index 1cda1d5461..14e6b4ab63 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs @@ -60,5 +60,9 @@ namespace Avalonia.Rendering.SceneGraph context.PopClip(); } } + + public void Dispose() + { + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 29c482c336..dfd45fba9c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -80,7 +81,7 @@ namespace Avalonia.Rendering.SceneGraph /// public void Dispose() { - // Nothing to do here as we allocate no unmanaged resources. + // Nothing to do here since we allocate no unmanaged resources. } /// @@ -102,7 +103,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(Transform, brush, pen, geometry)) + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) { Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); } @@ -113,11 +114,11 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { var next = NextDrawAs(); - if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect)) + if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect)) { Add(new ImageNode(Transform, source, opacity, sourceRect, destRect)); } @@ -128,7 +129,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) { // This method is currently only used to composite layers so shouldn't be called here. throw new NotSupportedException(); @@ -139,7 +140,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(Transform, pen, p1, p2)) + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) { Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); } @@ -154,7 +155,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, null, pen, rect, cornerRadius)) { Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush))); } @@ -169,7 +170,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(Transform, foreground, origin, text)) + if (next == null || !next.Item.Equals(Transform, foreground, origin, text)) { Add(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground))); } @@ -184,7 +185,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, brush, null, rect, cornerRadius)) { Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush))); } @@ -204,7 +205,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(null)) + if (next == null || !next.Item.Equals(null)) { Add(new ClipNode()); } @@ -219,9 +220,9 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(null)) + if (next == null || !next.Item.Equals(null)) { - Add(new GeometryClipNode()); + Add((new GeometryClipNode())); } else { @@ -234,7 +235,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(null)) + if (next == null || !next.Item.Equals(null)) { Add(new OpacityNode()); } @@ -249,7 +250,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(null, null)) + if (next == null || !next.Item.Equals(null, null)) { Add(new OpacityMaskNode()); } @@ -264,7 +265,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(clip)) + if (next == null || !next.Item.Equals(clip)) { Add(new ClipNode(clip)); } @@ -279,7 +280,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(clip)) + if (next == null || !next.Item.Equals(clip)) { Add(new GeometryClipNode(clip)); } @@ -294,7 +295,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(opacity)) + if (next == null || !next.Item.Equals(opacity)) { Add(new OpacityNode(opacity)); } @@ -309,7 +310,7 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Equals(mask, bounds)) + if (next == null || !next.Item.Equals(mask, bounds)) { Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); } @@ -341,7 +342,7 @@ namespace Avalonia.Rendering.SceneGraph foreach (var operation in Owner._node.DrawOperations) { - dirty.Add(operation.Bounds); + dirty.Add(operation.Item.Bounds); } Owner._node = Node; @@ -355,7 +356,15 @@ namespace Avalonia.Rendering.SceneGraph public int DrawOperationIndex { get; } } - private void Add(IDrawOperation node) + private void Add(IDrawOperation node) + { + using (var refCounted = RefCountable.Create(node)) + { + Add(refCounted); + } + } + + private void Add(IRef node) { if (_drawOperationindex < _node.DrawOperations.Count) { @@ -369,9 +378,9 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } - private T NextDrawAs() where T : class, IDrawOperation + private IRef NextDrawAs() where T : class, IDrawOperation { - return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as T : null; + return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; } private IDictionary CreateChildScene(IBrush brush) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index 4c6ed189ff..1a5a6fad3f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -22,5 +22,9 @@ namespace Avalonia.Rendering.SceneGraph public abstract bool HitTest(Point p); public abstract void Render(IDrawingContextImpl context); + + public virtual void Dispose() + { + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs index a11d641151..2c1decf5dc 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs @@ -60,5 +60,8 @@ namespace Avalonia.Rendering.SceneGraph context.PopGeometryClip(); } } + public void Dispose() + { + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs index 839fd9b0e5..f8d5bf5b7a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IDrawOperation + public interface IDrawOperation : IDisposable { /// /// Gets the bounds of the visible content in the node in global coordinates. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 234cadbf31..681f00799b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -12,7 +13,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph representing an . /// - public interface IVisualNode + public interface IVisualNode : IDisposable { /// /// Gets the visual to which the node relates. @@ -66,7 +67,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the drawing operations for the visual. /// - IReadOnlyList DrawOperations { get; } + IReadOnlyList> DrawOperations { get; } /// /// Sets up the drawing context for rendering the node's geometry. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 8291d1c0bb..06fdb3f86c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Rendering.SceneGraph { @@ -19,11 +20,11 @@ namespace Avalonia.Rendering.SceneGraph /// The draw opacity. /// The source rect. /// The destination rect. - public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) : base(destRect, transform, null) { Transform = transform; - Source = source; + Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; @@ -37,7 +38,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the image to draw. /// - public IBitmapImpl Source { get; } + public IRef Source { get; } /// /// Gets the draw opacity. @@ -67,10 +68,10 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) { return transform == Transform && - Equals(source, Source) && + Equals(source.Item, Source.Item) && opacity == Opacity && sourceRect == SourceRect && destRect == DestRect; @@ -87,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph /// public override bool HitTest(Point p) => Bounds.Contains(p); + + public override void Dispose() + { + Source?.Dispose(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs index 097bf15828..ef49ac8879 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs @@ -60,5 +60,9 @@ namespace Avalonia.Rendering.SceneGraph context.PopOpacity(); } } + + public void Dispose() + { + } } } 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/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 6bea4d9bd6..3cb7e53d21 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -15,12 +18,13 @@ namespace Avalonia.Rendering.SceneGraph internal class VisualNode : IVisualNode { private static readonly IReadOnlyList EmptyChildren = new IVisualNode[0]; - private static readonly IReadOnlyList EmptyDrawOperations = new IDrawOperation[0]; + private static readonly IReadOnlyList> EmptyDrawOperations = new IRef[0]; private Rect? _bounds; private double _opacity; private List _children; - private List _drawOperations; + private List> _drawOperations; + private IRef _drawOperationsRefCounter; private bool _drawOperationsCloned; private Matrix transformRestore; @@ -101,7 +105,7 @@ namespace Avalonia.Rendering.SceneGraph public IReadOnlyList Children => _children ?? EmptyChildren; /// - public IReadOnlyList DrawOperations => _drawOperations ?? EmptyDrawOperations; + public IReadOnlyList> DrawOperations => _drawOperations ?? EmptyDrawOperations; /// /// Adds a child to the collection. @@ -117,10 +121,10 @@ namespace Avalonia.Rendering.SceneGraph /// Adds an operation to the collection. /// /// The operation to add. - public void AddDrawOperation(IDrawOperation operation) + public void AddDrawOperation(IRef operation) { EnsureDrawOperationsCreated(); - _drawOperations.Add(operation); + _drawOperations.Add(operation.Clone()); } /// @@ -131,6 +135,7 @@ namespace Avalonia.Rendering.SceneGraph { EnsureChildrenCreated(); _children.Remove(child); + child.Dispose(); } /// @@ -141,7 +146,9 @@ namespace Avalonia.Rendering.SceneGraph public void ReplaceChild(int index, IVisualNode node) { EnsureChildrenCreated(); + var old = _children[index]; _children[index] = node; + old.Dispose(); } /// @@ -149,10 +156,12 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opeation to be replaced. /// The operation to add. - public void ReplaceDrawOperation(int index, IDrawOperation operation) + public void ReplaceDrawOperation(int index, IRef operation) { EnsureDrawOperationsCreated(); - _drawOperations[index] = operation; + var old = _drawOperations[index]; + _drawOperations[index] = operation.Clone(); + old.Dispose(); } /// @@ -165,6 +174,10 @@ namespace Avalonia.Rendering.SceneGraph if (first < _children?.Count) { EnsureChildrenCreated(); + for (int i = first; i < _children.Count - first; i++) + { + _children[i].Dispose(); + } _children.RemoveRange(first, _children.Count - first); } } @@ -179,6 +192,10 @@ namespace Avalonia.Rendering.SceneGraph if (first < _drawOperations?.Count) { EnsureDrawOperationsCreated(); + for (int i = first; i < _drawOperations.Count; i++) + { + _drawOperations[i].Dispose(); + } _drawOperations.RemoveRange(first, _drawOperations.Count - first); } } @@ -199,6 +216,7 @@ namespace Avalonia.Rendering.SceneGraph _opacity = Opacity, OpacityMask = OpacityMask, _drawOperations = _drawOperations, + _drawOperationsRefCounter = _drawOperationsRefCounter?.Clone(), _drawOperationsCloned = true, LayerRoot= LayerRoot, }; @@ -209,7 +227,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var operation in DrawOperations) { - if (operation.HitTest(p) == true) + if (operation.Item.HitTest(p) == true) { return true; } @@ -280,7 +298,7 @@ namespace Avalonia.Rendering.SceneGraph foreach (var operation in DrawOperations) { - result = result.Union(operation.Bounds); + result = result.Union(operation.Item.Bounds); } _bounds = result; @@ -299,13 +317,34 @@ namespace Avalonia.Rendering.SceneGraph { if (_drawOperations == null) { - _drawOperations = new List(); + _drawOperations = new List>(); + _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations)); + _drawOperationsCloned = false; } else if (_drawOperationsCloned) { - _drawOperations = new List(_drawOperations); + _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); + _drawOperationsRefCounter.Dispose(); + _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations)); _drawOperationsCloned = false; } } + + public void Dispose() + { + foreach (var child in Children) + { + child.Dispose(); + } + _drawOperationsRefCounter?.Dispose(); + } + + private void DisposeDrawOperations() + { + foreach (var operation in DrawOperations) + { + operation.Dispose(); + } + } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4defec4c07..22e5652cfb 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -6,6 +6,7 @@ using System.Linq; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Utilities; +using Avalonia.Utilities; namespace Avalonia.Skia { @@ -39,9 +40,9 @@ namespace Avalonia.Skia Canvas.Clear(color.ToSKColor()); } - public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { - var impl = (BitmapImpl)source; + var impl = (BitmapImpl)source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); using (var paint = new SKPaint() @@ -51,10 +52,10 @@ namespace Avalonia.Skia } } - public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); - DrawImage(source, 1, new Rect(0, 0, source.PixelWidth, source.PixelHeight), destRect); + DrawImage(source, 1, new Rect(0, 0, source.Item.PixelWidth, source.Item.PixelHeight), destRect); PopOpacityMask(); } @@ -237,7 +238,7 @@ namespace Avalonia.Skia } else { - tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl); + tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item); } if (tileBrush != null && tileBrushImage != null) @@ -252,7 +253,7 @@ namespace Avalonia.Skia context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(tileBrushImage, 1, rect, rect); + context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); context.PopClip(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 04303130e5..bdbbdab2b9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Utilities; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; @@ -99,9 +100,9 @@ namespace Avalonia.Direct2D1.Media /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { - using (var d2d = ((BitmapImpl)source).GetDirect2DBitmap(_renderTarget)) + using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_renderTarget)) { _renderTarget.DrawBitmap( d2d.Value, @@ -119,9 +120,9 @@ namespace Avalonia.Direct2D1.Media /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { - using (var d2dSource = ((BitmapImpl)source).GetDirect2DBitmap(_renderTarget)) + using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_renderTarget)) using (var sourceBrush = new BitmapBrush(_renderTarget, d2dSource.Value)) using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_renderTarget.Factory, destRect.ToDirect2D())) @@ -398,7 +399,7 @@ namespace Avalonia.Direct2D1.Media return new ImageBrushImpl( imageBrush, _renderTarget, - (BitmapImpl)imageBrush.Source.PlatformImpl, + (BitmapImpl)imageBrush.Source.PlatformImpl.Item, destinationSize); } else if (visualBrush != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 08cfed2ace..75b397edd6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Media; using Avalonia.Rendering.Utilities; +using Avalonia.Utilities; using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media @@ -100,7 +101,8 @@ namespace Avalonia.Direct2D1.Media context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(bitmap, 1, rect, rect); + + context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect); context.PopClip(); } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 8fcb54775b..7821f60a51 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -359,7 +359,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering private Mock GetLayerContext(DeferredRenderer renderer, IControl layerRoot) { - return Mock.Get(renderer.Layers[layerRoot].Bitmap.CreateDrawingContext(null)); + return Mock.Get(renderer.Layers[layerRoot].Bitmap.Item.CreateDrawingContext(null)); } private void IgnoreFirstFrame(Mock loop, Mock sceneBuilder) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs index 1906775783..4d23c57eed 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.UnitTests; +using Avalonia.Utilities; using Avalonia.VisualTree; using Moq; using Xunit; @@ -103,15 +105,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } Assert.Equal(2, node.DrawOperations.Count); - Assert.IsType(node.DrawOperations[0]); - Assert.IsType(node.DrawOperations[1]); + Assert.IsType(node.DrawOperations[0].Item); + Assert.IsType(node.DrawOperations[1].Item); } [Fact] public void Should_Not_Replace_Identical_DrawOperation() { var node = new VisualNode(new TestRoot(), null); - var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0); + 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); @@ -124,16 +126,16 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } Assert.Equal(1, node.DrawOperations.Count); - Assert.Same(operation, node.DrawOperations.Single()); + Assert.Same(operation.Item, node.DrawOperations.Single().Item); - Assert.IsType(node.DrawOperations[0]); + Assert.IsType(node.DrawOperations[0].Item); } [Fact] public void Should_Replace_Different_DrawOperation() { var node = new VisualNode(new TestRoot(), null); - var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0); + 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); @@ -148,7 +150,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(1, node.DrawOperations.Count); Assert.NotSame(operation, node.DrawOperations.Single()); - Assert.IsType(node.DrawOperations[0]); + Assert.IsType(node.DrawOperations[0].Item); } [Fact] @@ -173,13 +175,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Trim_DrawOperations() { var node = new VisualNode(new TestRoot(), null); - node.LayerRoot = node.Visual; - node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 10, 100), 0)); - node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 20, 100), 0)); - node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 30, 100), 0)); - node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 40, 100), 0)); + for (var i = 0; i < 4; ++i) + { + var drawOperation = new Mock(); + using (var r = RefCountable.Create(drawOperation.Object)) + { + node.AddDrawOperation(r); + } + } + + var drawOperations = node.DrawOperations.Select(op => op.Item).ToList(); var layers = new SceneLayers(node.Visual); var target = new DeferredDrawingContextImpl(null, layers); @@ -190,6 +197,33 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } Assert.Equal(2, node.DrawOperations.Count); + + foreach (var i in drawOperations) + { + Mock.Get(i).Verify(x => x.Dispose()); + } + } + + [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 44b9ebe8ce..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 { @@ -53,15 +55,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(1, borderNode.Children.Count); Assert.Equal(1, borderNode.DrawOperations.Count); - var backgroundNode = (RectangleNode)borderNode.DrawOperations[0]; + var backgroundNode = (RectangleNode)borderNode.DrawOperations[0].Item; Assert.Equal(Brushes.Red, backgroundNode.Brush); - var textBlockNode = (VisualNode)borderNode.Children[0]; + var textBlockNode = borderNode.Children[0]; Assert.Same(textBlockNode, result.FindNode(textBlock)); Assert.Same(textBlock, textBlockNode.Visual); Assert.Equal(1, textBlockNode.DrawOperations.Count); - var textNode = (TextNode)textBlockNode.DrawOperations[0]; + var textNode = (TextNode)textBlockNode.DrawOperations[0].Item; Assert.NotNull(textNode.Text); } } @@ -358,15 +360,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var borderNode = (VisualNode)result.Root.Children[0]; Assert.Same(border, borderNode.Visual); - var backgroundNode = (RectangleNode)borderNode.DrawOperations[0]; + var backgroundNode = (RectangleNode)borderNode.DrawOperations[0].Item; Assert.NotSame(initialBackgroundNode, backgroundNode); Assert.Equal(Brushes.Green, backgroundNode.Brush); var textBlockNode = (VisualNode)borderNode.Children[0]; Assert.Same(textBlock, textBlockNode.Visual); - var textNode = (TextNode)textBlockNode.DrawOperations[0]; - Assert.Same(initialTextNode, textNode); + var textNode = (TextNode)textBlockNode.DrawOperations[0].Item; + Assert.Same(initialTextNode.Item, textNode); } } @@ -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( diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs index 3d2e780e0b..1101ccacba 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; using Avalonia.VisualTree; using Moq; using Xunit; @@ -43,7 +44,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var node = new VisualNode(Mock.Of(), null); var collection = node.DrawOperations; - node.AddDrawOperation(Mock.Of()); + node.AddDrawOperation(RefCountable.Create(Mock.Of())); Assert.NotSame(collection, node.DrawOperations); } @@ -52,7 +53,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Cloned_Nodes_Should_Share_DrawOperations_Collection() { var node1 = new VisualNode(Mock.Of(), null); - node1.AddDrawOperation(Mock.Of()); + node1.AddDrawOperation(RefCountable.Create(Mock.Of())); var node2 = node1.Clone(null); @@ -63,18 +64,33 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Adding_DrawOperation_To_Cloned_Node_Should_Create_New_Collection() { var node1 = new VisualNode(Mock.Of(), null); - var operation1 = Mock.Of(); + var operation1 = RefCountable.Create(Mock.Of()); node1.AddDrawOperation(operation1); var node2 = node1.Clone(null); - var operation2 = Mock.Of(); + var operation2 = RefCountable.Create(Mock.Of()); node2.ReplaceDrawOperation(0, operation2); Assert.NotSame(node1.DrawOperations, node2.DrawOperations); Assert.Equal(1, node1.DrawOperations.Count); Assert.Equal(1, node2.DrawOperations.Count); - Assert.Same(operation1, node1.DrawOperations[0]); - Assert.Same(operation2, node2.DrawOperations[0]); + Assert.Same(operation1.Item, node1.DrawOperations[0].Item); + Assert.Same(operation2.Item, node2.DrawOperations[0].Item); + } + + [Fact] + public void DrawOperations_In_Cloned_Node_Are_Cloned() + { + var node1 = new VisualNode(Mock.Of(), null); + var operation1 = RefCountable.Create(Mock.Of()); + node1.AddDrawOperation(operation1); + + var node2 = node1.Clone(null); + var operation2 = RefCountable.Create(Mock.Of()); + node2.AddDrawOperation(operation2); + + Assert.Same(node1.DrawOperations[0].Item, node2.DrawOperations[0].Item); + Assert.NotSame(node1.DrawOperations[0], node2.DrawOperations[0]); } } }