27 changed files with 806 additions and 154 deletions
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Threading |
|||
{ |
|||
/// <summary>
|
|||
/// Dispatches jobs to a thread.
|
|||
/// </summary>
|
|||
public interface IDispatcher |
|||
{ |
|||
/// <summary>
|
|||
/// Determines whether the calling thread is the thread associated with this <see cref="IDispatcher"/>.
|
|||
/// </summary>
|
|||
/// <returns>True if he calling thread is the thread associated with the dispatched, otherwise false.</returns>
|
|||
bool CheckAccess(); |
|||
|
|||
/// <summary>
|
|||
/// Throws an exception if the calling thread is not the thread associated with this <see cref="IDispatcher"/>.
|
|||
/// </summary>
|
|||
void VerifyAccess(); |
|||
|
|||
/// <summary>
|
|||
/// Invokes a method on the dispatcher thread.
|
|||
/// </summary>
|
|||
/// <param name="action">The method.</param>
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
/// <returns>A task that can be used to track the method's execution.</returns>
|
|||
void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); |
|||
|
|||
/// <summary>
|
|||
/// Post action that will be invoked on main thread
|
|||
/// </summary>
|
|||
/// <param name="action">The method.</param>
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
// TODO: The naming of this method is confusing: the Async suffix usually means return a task.
|
|||
// Remove this and rename InvokeTaskAsync as InvokeAsync. See #816.
|
|||
Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class DefaultRenderLayerFactory : IRenderLayerFactory |
|||
{ |
|||
private IPlatformRenderInterface _renderInterface; |
|||
|
|||
public DefaultRenderLayerFactory() |
|||
: this(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()) |
|||
{ |
|||
} |
|||
|
|||
public DefaultRenderLayerFactory(IPlatformRenderInterface renderInterface) |
|||
{ |
|||
_renderInterface = renderInterface; |
|||
} |
|||
|
|||
public IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size) |
|||
{ |
|||
return _renderInterface.CreateRenderTargetBitmap( |
|||
(int)Math.Ceiling(size.Width), |
|||
(int)Math.Ceiling(size.Height)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public interface IRenderLayerFactory |
|||
{ |
|||
IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size); |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class LayerDirtyRects : Dictionary<IVisual, DirtyRects> |
|||
{ |
|||
public bool IsEmpty |
|||
{ |
|||
get |
|||
{ |
|||
foreach (var i in Values) |
|||
{ |
|||
if (!i.IsEmpty) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
public void Add(IVisual layerRoot, Rect rect) |
|||
{ |
|||
DirtyRects rects; |
|||
|
|||
if (!TryGetValue(layerRoot, out rects)) |
|||
{ |
|||
Add(layerRoot, rects = new DirtyRects()); |
|||
} |
|||
|
|||
rects.Add(rect); |
|||
} |
|||
|
|||
public void Coalesce() |
|||
{ |
|||
foreach (var i in Values) |
|||
{ |
|||
i.Coalesce(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class RenderLayer : IComparable<RenderLayer> |
|||
{ |
|||
public RenderLayer( |
|||
IRenderTargetBitmapImpl bitmap, |
|||
Size size, |
|||
IVisual layerRoot) |
|||
{ |
|||
Bitmap = bitmap; |
|||
Size = size; |
|||
LayerRoot = layerRoot; |
|||
Order = GetDistanceFromRenderRoot(layerRoot); |
|||
} |
|||
|
|||
public IRenderTargetBitmapImpl Bitmap { get; } |
|||
public Size Size { get; } |
|||
public IVisual LayerRoot { get; } |
|||
public int Order { get; } |
|||
|
|||
private static int GetDistanceFromRenderRoot(IVisual visual) |
|||
{ |
|||
var root = visual as IRenderRoot; |
|||
var result = 0; |
|||
|
|||
while (root == null) |
|||
{ |
|||
++result; |
|||
visual = visual.VisualParent; |
|||
|
|||
if (visual == null) |
|||
{ |
|||
throw new AvaloniaInternalException("Visual is not rooted."); |
|||
} |
|||
|
|||
root = visual as IRenderRoot; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public int CompareTo(RenderLayer other) |
|||
{ |
|||
return Order - other.Order; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class RenderLayers : IEnumerable<RenderLayer> |
|||
{ |
|||
private readonly IRenderLayerFactory _factory; |
|||
private List<RenderLayer> _inner = new List<RenderLayer>(); |
|||
private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>(); |
|||
|
|||
public RenderLayers(IRenderLayerFactory factory) |
|||
{ |
|||
_factory = factory; |
|||
} |
|||
|
|||
public RenderLayer Add(IVisual layerRoot, Size size) |
|||
{ |
|||
RenderLayer result; |
|||
|
|||
if (!_index.TryGetValue(layerRoot, out result)) |
|||
{ |
|||
var bitmap = _factory.CreateLayer(layerRoot, size); |
|||
result = new RenderLayer(bitmap, size, layerRoot); |
|||
_inner.Add(result); |
|||
_index.Add(layerRoot, result); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public RenderLayer Get(IVisual layerRoot) |
|||
{ |
|||
RenderLayer result; |
|||
_index.TryGetValue(layerRoot, out result); |
|||
return result; |
|||
} |
|||
|
|||
public void RemoveUnused(Scene scene) |
|||
{ |
|||
for (var i = _inner.Count - 1; i >= 0; --i) |
|||
{ |
|||
var layer = _inner[i]; |
|||
var node = (VisualNode)scene.FindNode(layer.LayerRoot); |
|||
|
|||
if (node == null || node.LayerRoot != layer.LayerRoot) |
|||
{ |
|||
layer.Bitmap.Dispose(); |
|||
_inner.RemoveAt(i); |
|||
_index.Remove(layer.LayerRoot); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator(); |
|||
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
public interface ISceneBuilder |
|||
{ |
|||
bool Update(Scene scene, IVisual visual, LayerDirtyRects dirty); |
|||
void UpdateAll(Scene scene, LayerDirtyRects dirty); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.UnitTests |
|||
{ |
|||
/// <summary>
|
|||
/// Immediately invokes dispatched jobs on the current thread.
|
|||
/// </summary>
|
|||
public class ImmediateDispatcher : IDispatcher |
|||
{ |
|||
public bool CheckAccess() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) |
|||
{ |
|||
action(); |
|||
} |
|||
|
|||
public Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) |
|||
{ |
|||
action(); |
|||
return Task.FromResult<object>(null); |
|||
} |
|||
|
|||
public void VerifyAccess() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,240 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.VisualTree; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Rendering |
|||
{ |
|||
public class DeferredRendererTests |
|||
{ |
|||
[Fact] |
|||
public void First_Frame_Calls_UpdateScene_On_Dispatcher() |
|||
{ |
|||
var loop = new Mock<IRenderLoop>(); |
|||
var root = new TestRoot(); |
|||
var dispatcher = new Mock<IDispatcher>(); |
|||
var target = new DeferredRenderer( |
|||
root, |
|||
loop.Object, |
|||
sceneBuilder: null, |
|||
dispatcher: dispatcher.Object); |
|||
|
|||
RunFrame(loop); |
|||
|
|||
dispatcher.Verify(x => |
|||
x.InvokeAsync( |
|||
It.Is<Action>(a => a.Method.Name == "UpdateScene"), |
|||
DispatcherPriority.Render)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void First_Frame_Calls_SceneBuilder_UpdateAll() |
|||
{ |
|||
var loop = new Mock<IRenderLoop>(); |
|||
var root = new TestRoot(); |
|||
var sceneBuilder = new Mock<ISceneBuilder>(); |
|||
var dispatcher = new ImmediateDispatcher(); |
|||
var target = new DeferredRenderer( |
|||
root, |
|||
loop.Object, |
|||
sceneBuilder: sceneBuilder.Object, |
|||
dispatcher: dispatcher); |
|||
|
|||
RunFrame(loop); |
|||
|
|||
sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>(), It.IsAny<LayerDirtyRects>())); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Frame_Does_Not_Call_SceneBuilder_If_No_Dirty_Controls() |
|||
{ |
|||
var loop = new Mock<IRenderLoop>(); |
|||
var root = new TestRoot(); |
|||
var sceneBuilder = new Mock<ISceneBuilder>(); |
|||
var dispatcher = new ImmediateDispatcher(); |
|||
var target = new DeferredRenderer( |
|||
root, |
|||
loop.Object, |
|||
sceneBuilder: sceneBuilder.Object, |
|||
dispatcher: dispatcher); |
|||
|
|||
IgnoreFirstFrame(loop, sceneBuilder); |
|||
RunFrame(loop); |
|||
|
|||
sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>(), It.IsAny<LayerDirtyRects>()), Times.Never); |
|||
sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), It.IsAny<Visual>(), It.IsAny<LayerDirtyRects>()), Times.Never); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Frame_Should_Call_SceneBuilder_Update_With_Dirty_Controls() |
|||
{ |
|||
var loop = new Mock<IRenderLoop>(); |
|||
var root = new TestRoot(); |
|||
var sceneBuilder = new Mock<ISceneBuilder>(); |
|||
var dispatcher = new ImmediateDispatcher(); |
|||
var control1 = new Border(); |
|||
var control2 = new Canvas(); |
|||
var target = new DeferredRenderer( |
|||
root, |
|||
loop.Object, |
|||
sceneBuilder: sceneBuilder.Object, |
|||
dispatcher: dispatcher); |
|||
|
|||
IgnoreFirstFrame(loop, sceneBuilder); |
|||
target.AddDirty(control1); |
|||
target.AddDirty(control2); |
|||
RunFrame(loop); |
|||
|
|||
sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), control1, It.IsAny<LayerDirtyRects>())); |
|||
sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), control2, It.IsAny<LayerDirtyRects>())); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Frame_Should_Create_Layer_For_Root() |
|||
{ |
|||
var loop = new Mock<IRenderLoop>(); |
|||
var root = new TestRoot(); |
|||
var rootLayer = new Mock<IRenderTargetBitmapImpl>(); |
|||
var dispatcher = new ImmediateDispatcher(); |
|||
|
|||
var sceneBuilder = new Mock<ISceneBuilder>(); |
|||
sceneBuilder.Setup(x => x.UpdateAll(It.IsAny<Scene>(), It.IsAny<LayerDirtyRects>())) |
|||
.Callback<Scene, LayerDirtyRects>((scene, dirty) => |
|||
{ |
|||
var rects = new DirtyRects(); |
|||
rects.Add(new Rect(root.ClientSize)); |
|||
dirty.Add(root, rects); |
|||
}); |
|||
|
|||
var layers = new Mock<IRenderLayerFactory>(); |
|||
layers.Setup(x => x.CreateLayer(root, root.ClientSize)).Returns(CreateLayer()); |
|||
|
|||
var renderInterface = new Mock<IPlatformRenderInterface>(); |
|||
|
|||
var target = new DeferredRenderer( |
|||
root, |
|||
loop.Object, |
|||
sceneBuilder: sceneBuilder.Object, |
|||
layerFactory: layers.Object, |
|||
dispatcher: dispatcher); |
|||
|
|||
RunFrame(loop); |
|||
|
|||
layers.Verify(x => x.CreateLayer(root, root.ClientSize)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Create_And_Delete_Layers_For_Transparent_Controls() |
|||
{ |
|||
Border border; |
|||
var root = new TestRoot |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Child = new Border |
|||
{ |
|||
Background = Brushes.Red, |
|||
Child = border = new Border |
|||
{ |
|||
Background = Brushes.Green, |
|||
} |
|||
} |
|||
}; |
|||
|
|||
root.Measure(Size.Infinity); |
|||
root.Arrange(new Rect(root.DesiredSize)); |
|||
|
|||
var loop = new Mock<IRenderLoop>(); |
|||
var layerFactory = new MockRenderLayerFactory(new Dictionary<IVisual, IRenderTargetBitmapImpl> |
|||
{ |
|||
{ root, CreateLayer() }, |
|||
{ border, CreateLayer() }, |
|||
}); |
|||
|
|||
var target = new DeferredRenderer( |
|||
root, |
|||
loop.Object, |
|||
layerFactory: layerFactory, |
|||
dispatcher: new ImmediateDispatcher()); |
|||
root.Renderer = target; |
|||
|
|||
RunFrame(loop); |
|||
|
|||
var rootContext = layerFactory.GetMockDrawingContext(root); |
|||
var borderContext = layerFactory.GetMockDrawingContext(border); |
|||
|
|||
rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); |
|||
rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once); |
|||
borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never); |
|||
|
|||
rootContext.ResetCalls(); |
|||
borderContext.ResetCalls(); |
|||
border.Opacity = 0.5; |
|||
RunFrame(loop); |
|||
|
|||
rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); |
|||
rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Never); |
|||
borderContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once); |
|||
|
|||
rootContext.ResetCalls(); |
|||
borderContext.ResetCalls(); |
|||
border.Opacity = 1; |
|||
RunFrame(loop); |
|||
|
|||
layerFactory.GetMockBitmap(border).Verify(x => x.Dispose()); |
|||
rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); |
|||
rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once); |
|||
borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never); |
|||
} |
|||
|
|||
private void IgnoreFirstFrame(Mock<IRenderLoop> loop, Mock<ISceneBuilder> sceneBuilder) |
|||
{ |
|||
RunFrame(loop); |
|||
sceneBuilder.ResetCalls(); |
|||
} |
|||
|
|||
private void RunFrame(Mock<IRenderLoop> loop) |
|||
{ |
|||
loop.Raise(x => x.Tick += null, EventArgs.Empty); |
|||
} |
|||
|
|||
private IRenderTargetBitmapImpl CreateLayer() |
|||
{ |
|||
return Mock.Of<IRenderTargetBitmapImpl>(x => |
|||
x.CreateDrawingContext() == Mock.Of<IDrawingContextImpl>()); |
|||
} |
|||
|
|||
private class MockRenderLayerFactory : IRenderLayerFactory |
|||
{ |
|||
private IDictionary<IVisual, IRenderTargetBitmapImpl> _layers; |
|||
|
|||
public MockRenderLayerFactory(IDictionary<IVisual, IRenderTargetBitmapImpl> layers) |
|||
{ |
|||
_layers = layers; |
|||
} |
|||
|
|||
public IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size) |
|||
{ |
|||
return _layers[layerRoot]; |
|||
} |
|||
|
|||
public Mock<IRenderTargetBitmapImpl> GetMockBitmap(IVisual layerRoot) |
|||
{ |
|||
return Mock.Get(_layers[layerRoot]); |
|||
} |
|||
|
|||
public Mock<IDrawingContextImpl> GetMockDrawingContext(IVisual layerRoot) |
|||
{ |
|||
return Mock.Get(_layers[layerRoot].CreateDrawingContext()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue