diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index a850b99c5e..d920be2706 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using Avalonia.Logging; using Avalonia.Threading; @@ -89,18 +90,7 @@ namespace Avalonia.Rendering { try { - var needsUpdate = false; - - foreach (var i in _items) - { - if (i.NeedsUpdate) - { - needsUpdate = true; - break; - } - } - - if (needsUpdate) + if (_items.Any(item => item.NeedsUpdate)) { await _dispatcher.InvokeAsync(() => { @@ -108,7 +98,7 @@ namespace Avalonia.Rendering { i.Update(tickCount); } - }).ConfigureAwait(false); + }, DispatcherPriority.Render).ConfigureAwait(false); } foreach (var i in _items) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 1af9a9499d..e2a5c0c54c 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; - +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Media; @@ -22,27 +22,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering { public class DeferredRendererTests { - [Fact] - public void First_Frame_Calls_UpdateScene_On_Dispatcher() - { - var root = new TestRoot(); - - var dispatcher = new Mock(); - dispatcher.Setup(x => x.Post(It.IsAny(), DispatcherPriority.Render)) - .Callback((a, p) => a()); - - CreateTargetAndRunFrame(root, dispatcher: dispatcher.Object); - - dispatcher.Verify(x => - x.Post( - It.Is(a => a.Method.Name == "UpdateScene"), - DispatcherPriority.Render)); - } - [Fact] public void First_Frame_Calls_SceneBuilder_UpdateAll() { - var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); @@ -54,6 +36,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Frame_Does_Not_Call_SceneBuilder_If_No_Dirty_Controls() { + var dispatcher = new ImmediateDispatcher(); var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); @@ -63,8 +46,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering sceneBuilder: sceneBuilder.Object); target.Start(); - IgnoreFirstFrame(loop, sceneBuilder); - RunFrame(loop); + IgnoreFirstFrame(target, sceneBuilder); + RunFrame(target); sceneBuilder.Verify(x => x.UpdateAll(It.IsAny()), Times.Never); sceneBuilder.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.Never); @@ -73,8 +56,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Should_Update_Dirty_Controls_In_Order() { - var loop = new Mock(); var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); Border border; Decorator decorator; @@ -98,7 +81,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering dispatcher: dispatcher); target.Start(); - IgnoreFirstFrame(loop, sceneBuilder); + IgnoreFirstFrame(target, sceneBuilder); target.AddDirty(border); target.AddDirty(canvas); target.AddDirty(root); @@ -108,7 +91,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering sceneBuilder.Setup(x => x.Update(It.IsAny(), It.IsAny())) .Callback((_, v) => result.Add(v)); - RunFrame(loop); + RunFrame(target); Assert.Equal(new List { root, decorator, border, canvas }, result); } @@ -198,7 +181,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Should_Create_Layer_For_Root() { - var loop = new Mock(); var root = new TestRoot(); var rootLayer = new Mock(); @@ -239,19 +221,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var loop = new Mock(); - var target = CreateTargetAndRunFrame(root, loop: loop); + var timer = new Mock(); + var target = CreateTargetAndRunFrame(root, timer); Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); - RunFrame(loop); + RunFrame(target); Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot)); animation.OnCompleted(); - RunFrame(loop); + RunFrame(target); Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); } @@ -280,8 +262,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var loop = new Mock(); - var target = CreateTargetAndRunFrame(root, loop: loop); + var timer = new Mock(); + var target = CreateTargetAndRunFrame(root, timer); Assert.Single(target.Layers); } @@ -345,19 +327,20 @@ namespace Avalonia.Visuals.UnitTests.Rendering private DeferredRenderer CreateTargetAndRunFrame( TestRoot root, - Mock loop = null, + Mock timer = null, ISceneBuilder sceneBuilder = null, IDispatcher dispatcher = null) { - loop = loop ?? new Mock(); + timer = timer ?? new Mock(); + dispatcher = dispatcher ?? new ImmediateDispatcher(); var target = new DeferredRenderer( root, - loop.Object, + new RenderLoop(timer.Object, dispatcher), sceneBuilder: sceneBuilder, - dispatcher: dispatcher ?? new ImmediateDispatcher()); + dispatcher: dispatcher); root.Renderer = target; target.Start(); - RunFrame(loop); + RunFrame(target); return target; } @@ -366,15 +349,16 @@ namespace Avalonia.Visuals.UnitTests.Rendering return Mock.Get(renderer.Layers[layerRoot].Bitmap.Item.CreateDrawingContext(null)); } - private void IgnoreFirstFrame(Mock loop, Mock sceneBuilder) + private void IgnoreFirstFrame(IRenderLoopTask task, Mock sceneBuilder) { - RunFrame(loop); + RunFrame(task); sceneBuilder.ResetCalls(); } - private void RunFrame(Mock loop) + private void RunFrame(IRenderLoopTask task) { - //loop.Raise(x => x.Tick += null, EventArgs.Empty); + task.Update(0); + task.Render(); } private IRenderTargetBitmapImpl CreateLayer() diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs new file mode 100644 index 0000000000..30ef35a2bb --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Rendering; +using Avalonia.Threading; +using Moq; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering +{ + public class RenderLoopTests + { + [Fact] + public void RenderLoop_Update_Runs_On_Dispatcher() + { + var dispatcher = new Mock(); + + bool inDispatcher = false; + + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => + { + inDispatcher = true; + a(); + inDispatcher = false; + }) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + + var loop = new RenderLoop(timer.Object, dispatcher.Object); + + var renderTask = new Mock(); + + renderTask.Setup(t => t.NeedsUpdate).Returns(true); + renderTask.Setup(t => t.Update(It.IsAny())) + .Callback((long _) => Assert.True(inDispatcher)); + + loop.Add(renderTask.Object); + + timer.Raise(t => t.Tick += null, 0L); + + renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); + } + + [Fact] + public void RenderLoop_Does_Not_Update_When_No_Tasks_Need_Update() + { + var dispatcher = new Mock(); + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => a()) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + var loop = new RenderLoop(timer.Object, dispatcher.Object); + var renderTask = new Mock(); + renderTask.Setup(t => t.NeedsUpdate).Returns(false); + + loop.Add(renderTask.Object); + timer.Raise(t => t.Tick += null, 0L); + + renderTask.Verify(t => t.Update(It.IsAny()), Times.Never()); + } + + [Fact] + public void RenderLoop_Render_Runs_Off_Dispatcher() + { + var dispatcher = new Mock(); + bool inDispatcher = false; + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => + { + inDispatcher = true; + a(); + inDispatcher = false; + }) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + var loop = new RenderLoop(timer.Object, dispatcher.Object); + + var renderTask = new Mock(); + + renderTask.Setup(t => t.NeedsUpdate).Returns(true); + renderTask.Setup(t => t.Render()) + .Callback(() => Assert.False(inDispatcher)); + + loop.Add(renderTask.Object); + timer.Raise(t => t.Tick += null, 0L); + + renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); + } + + [Fact] + public void RenderLoop_Passes_Tick_Count_To_Update() + { + var dispatcher = new Mock(); + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => a()) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + var loop = new RenderLoop(timer.Object, dispatcher.Object); + var renderTask = new Mock(); + renderTask.Setup(t => t.NeedsUpdate).Returns(true); + + loop.Add(renderTask.Object); + var tickCount = 12345L; + timer.Raise(t => t.Tick += null, tickCount); + + renderTask.Verify(t => t.Update(tickCount), Times.Once()); + } + } +}