From 409e40c8becdbeaf84c4de5223588cc91ae4c85b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Dec 2017 19:14:29 +0100 Subject: [PATCH 01/10] Added AvaloniaObject.IsAnimating(property). Added a method to test whether a property on an object is currently animating. --- src/Avalonia.Base/AvaloniaObject.cs | 13 ++++++ src/Avalonia.Base/IAvaloniaObject.cs | 7 ++++ src/Avalonia.Base/PriorityValue.cs | 2 + .../AvaloniaObjectTests_Binding.cs | 40 +++++++++++++++++++ .../DirectPropertyTests.cs | 12 ++++++ .../SelectorTests_Child.cs | 5 +++ .../SelectorTests_Descendent.cs | 5 +++ .../TestControlBase.cs | 5 +++ .../TestTemplatedControl.cs | 5 +++ 9 files changed, 94 insertions(+) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index efcbb57244..8b93cf2fb1 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -225,6 +225,19 @@ namespace Avalonia return (T)GetValue((AvaloniaProperty)property); } + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + public bool IsAnimating(AvaloniaProperty property) + { + Contract.Requires(property != null); + VerifyAccess(); + + return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; + } + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11bab2236..c11f8ada7e 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -31,6 +31,13 @@ namespace Avalonia /// The value. T GetValue(AvaloniaProperty property); + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + bool IsAnimating(AvaloniaProperty property); + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 3726fb7ae5..e1b62c030c 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -52,6 +52,8 @@ namespace Avalonia _validate = validate; } + public bool IsAnimating => ValuePriority < 0 && GetLevel(ValuePriority).ActiveBindingIndex != -1; + /// /// Gets the owner of the value. /// diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 4d6559a078..cd77d9cc88 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -387,6 +387,46 @@ namespace Avalonia.Base.UnitTests } } + [Fact] + public void IsAnimating_On_Property_With_No_Value_Returns_False() + { + var target = new Class1(); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Animation_Value_Returns_False() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Non_Animation_Binding_Returns_False() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source, BindingPriority.LocalValue); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Animation_Binding_Returns_True() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + /// /// Returns an observable that returns a single value but does not complete. /// diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index 3a37585dc0..84ff492512 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Subjects; using Avalonia.Data; using Xunit; @@ -70,6 +71,17 @@ namespace Avalonia.Base.UnitTests Assert.Same(p1.Initialized, p2.Initialized); } + [Fact] + public void IsAnimating_On_DirectProperty_With_Binding_Returns_False() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 352efe5358..d837f2cb2f 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -135,6 +135,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public bool IsAnimating(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + public bool IsSet(AvaloniaProperty property) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index c413904c8f..b75b59c212 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -165,6 +165,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public bool IsAnimating(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + public bool IsSet(AvaloniaProperty property) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs index f57ac836fb..82be755a39 100644 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs @@ -59,6 +59,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public bool IsAnimating(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + public bool IsSet(AvaloniaProperty property) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs index 07a988f410..03b2f03bf2 100644 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs @@ -67,6 +67,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public bool IsAnimating(AvaloniaProperty property) + { + throw new NotImplementedException(); + } + public bool IsSet(AvaloniaProperty property) { throw new NotImplementedException(); From 05575f9ea843fffa0980acc12a752ec408c17677 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Dec 2017 22:08:55 +0100 Subject: [PATCH 02/10] Display # of layers next to FPS counter. --- .../Rendering/DeferredRenderer.cs | 1 + .../Rendering/ImmediateRenderer.cs | 2 +- src/Avalonia.Visuals/Rendering/RendererBase.cs | 17 +++++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index f7befa646a..599b224b63 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -354,6 +354,7 @@ namespace Avalonia.Rendering if (DrawFps) { RenderFps(context, clientRect, true); + RenderFps(context, clientRect, scene.Layers.Count); } } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 2d5a864089..84313f0906 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -69,7 +69,7 @@ namespace Avalonia.Rendering if (DrawFps) { - RenderFps(context.PlatformImpl, _root.Bounds, true); + RenderFps(context.PlatformImpl, _root.Bounds, null); } } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 707b31998a..eac362e997 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -22,15 +22,12 @@ namespace Avalonia.Rendering }; } - protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount) + protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (incrementFrameCount) - { - ++_framesThisSecond; - } + ++_framesThisSecond; if (elapsed.TotalSeconds > 1) { @@ -39,7 +36,15 @@ namespace Avalonia.Rendering _lastFpsUpdate = now; } - _fpsText.Text = string.Format("FPS: {0:000}", _fps); + if (layerCount.HasValue) + { + _fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps); + } + else + { + _fpsText.Text = string.Format("FPS: {0:000}", _fps); + } + var size = _fpsText.Measure(); var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); From 68bdbca8992ee35d9aa482a69fe24504cf1c1f88 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Dec 2017 22:09:12 +0100 Subject: [PATCH 03/10] Only create render layers when Opacity is animating. Previously we were creating a new render layer for all controls with `Opacity != 1`. This was causing the `Calendar` page in ControlCatalog to render _really_ slowly. Instead, only create a new render layer when `Opacity` is being animated. Also don't even render controls where `Opacity == 0`. --- .../Rendering/DeferredRenderer.cs | 3 +- .../Rendering/SceneGraph/SceneBuilder.cs | 2 +- .../Rendering/SceneGraph/VisualNode.cs | 10 ++ .../Rendering/DeferredRendererTests.cs | 98 ++++++++++++++++- .../Rendering/SceneGraph/SceneBuilderTests.cs | 6 +- .../SceneGraph/SceneBuilderTests_Layers.cs | 100 ++++-------------- 6 files changed, 131 insertions(+), 88 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 599b224b63..cba09297f3 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -235,7 +235,7 @@ namespace Avalonia.Rendering { clipBounds = node.ClipBounds.Intersect(clipBounds); - if (!clipBounds.IsEmpty) + if (!clipBounds.IsEmpty && node.Opacity > 0) { node.BeginRender(context); @@ -353,7 +353,6 @@ namespace Avalonia.Rendering if (DrawFps) { - RenderFps(context, clientRect, true); RenderFps(context, clientRect, scene.Layers.Count); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index b5ac7e4077..6c92ff022c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -167,7 +167,7 @@ namespace Avalonia.Rendering.SceneGraph using (context.PushPostTransform(m)) using (context.PushTransformContainer()) { - var startLayer = opacity < 1 || visual.OpacityMask != null; + var startLayer = (visual as IAvaloniaObject)?.IsAnimating(Visual.OpacityProperty) ?? false; var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); forceRecurse = forceRecurse || diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index dd5740e4a9..ea3b07ffe3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -226,6 +226,11 @@ namespace Avalonia.Rendering.SceneGraph context.PushClip(ClipBounds); } + if (Opacity != 1) + { + context.PushOpacity(Opacity); + } + context.Transform = Transform; if (GeometryClip != null) @@ -242,6 +247,11 @@ namespace Avalonia.Rendering.SceneGraph context.PopGeometryClip(); } + if (Opacity != 1) + { + context.PopOpacity(); + } + if (ClipToBounds) { context.PopClip(); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index cec95d4807..9d90ae23af 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -126,6 +128,94 @@ namespace Avalonia.Visuals.UnitTests.Rendering Assert.Equal(new List { root, decorator, border, canvas }, result); } + [Fact] + public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() + { + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Border + { + Background = Brushes.Red, + Opacity = 0.5, + } + }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var rootLayer = CreateLayer(); + var borderLayer = CreateLayer(); + var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); + renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny())) + .Returns(rootLayer) + .Returns(borderLayer); + + var loop = new Mock(); + var target = new DeferredRenderer( + root, + loop.Object, + dispatcher: new ImmediateDispatcher()); + root.Renderer = target; + + target.Start(); + RunFrame(loop); + + var context = Mock.Get(rootLayer.CreateDrawingContext(null)); + var animation = new BehaviorSubject(0.5); + + context.Verify(x => x.PushOpacity(0.5), Times.Once); + context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); + context.Verify(x => x.PopOpacity(), Times.Once); + } + + [Fact] + public void Should_Not_Draw_Controls_With_0_Opacity() + { + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Border + { + Background = Brushes.Red, + Opacity = 0, + Child = new Border + { + Background = Brushes.Green, + } + } + }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var rootLayer = CreateLayer(); + var borderLayer = CreateLayer(); + var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); + renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny())) + .Returns(rootLayer) + .Returns(borderLayer); + + var loop = new Mock(); + var target = new DeferredRenderer( + root, + loop.Object, + dispatcher: new ImmediateDispatcher()); + root.Renderer = target; + + target.Start(); + RunFrame(loop); + + var context = Mock.Get(rootLayer.CreateDrawingContext(null)); + var animation = new BehaviorSubject(0.5); + + context.Verify(x => x.PushOpacity(0.5), Times.Never); + context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Never); + context.Verify(x => x.PopOpacity(), Times.Never); + } + [Fact] public void Frame_Should_Create_Layer_For_Root() { @@ -148,7 +238,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, - //layerFactory: layers.Object, dispatcher: dispatcher); target.Start(); @@ -159,7 +248,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } [Fact] - public void Should_Create_And_Delete_Layers_For_Transparent_Controls() + public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity() { Border border; var root = new TestRoot @@ -198,6 +287,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var rootContext = Mock.Get(rootLayer.CreateDrawingContext(null)); var borderContext = Mock.Get(borderLayer.CreateDrawingContext(null)); + var animation = new BehaviorSubject(0.5); 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); @@ -205,7 +295,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering rootContext.ResetCalls(); borderContext.ResetCalls(); - border.Opacity = 0.5; + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); RunFrame(loop); rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); @@ -214,7 +304,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering rootContext.ResetCalls(); borderContext.ResetCalls(); - border.Opacity = 1; + animation.OnCompleted(); RunFrame(loop); Mock.Get(borderLayer).Verify(x => x.Dispose()); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index d0f7671956..44b9ebe8ce 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -9,6 +9,8 @@ using Xunit; using Avalonia.Layout; using Moq; using Avalonia.Platform; +using System.Reactive.Subjects; +using Avalonia.Data; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { @@ -620,13 +622,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Margin = new Thickness(0, 10, 0, 0), Child = border = new Border { - Opacity = 0.5, Background = Brushes.Red, Child = canvas = new Canvas(), } } }; + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); sceneBuilder.UpdateAll(scene); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index ed74f12075..10372c7d8c 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -7,14 +7,15 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; using Avalonia.Layout; -using Avalonia.Rendering; +using System.Reactive.Subjects; +using Avalonia.Data; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { public partial class SceneBuilderTests { [Fact] - public void Control_With_Transparency_Should_Start_New_Layer() + public void Control_With_Animated_Opacity_Should_Start_New_Layer() { using (TestApplication()) { @@ -31,7 +32,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Padding = new Thickness(11), Child = border = new Border { - Opacity = 0.5, Background = Brushes.Red, Padding = new Thickness(12), Child = canvas = new Canvas(), @@ -42,6 +42,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var layout = AvaloniaLocator.Current.GetService(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); sceneBuilder.UpdateAll(scene); @@ -58,7 +61,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(2, scene.Layers.Count()); Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border })); - border.Opacity = 1; + animation.OnCompleted(); scene = scene.Clone(); sceneBuilder.Update(scene, border); @@ -80,7 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } [Fact] - public void Control_With_OpacityMask_Should_Start_New_Layer() + public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers() { using (TestApplication()) { @@ -97,10 +100,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Padding = new Thickness(11), Child = border = new Border { - OpacityMask = Brushes.Red, Background = Brushes.Red, Padding = new Thickness(12), - Child = canvas = new Canvas(), + Child = canvas = new Canvas() } } }; @@ -108,74 +110,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var layout = AvaloniaLocator.Current.GetService(); layout.ExecuteInitialLayoutPass(tree); - var scene = new Scene(tree); - var sceneBuilder = new SceneBuilder(); - sceneBuilder.UpdateAll(scene); - - var rootNode = (VisualNode)scene.Root; - var borderNode = (VisualNode)scene.FindNode(border); - var canvasNode = (VisualNode)scene.FindNode(canvas); - - Assert.Same(tree, rootNode.LayerRoot); - Assert.Same(border, borderNode.LayerRoot); - Assert.Same(border, canvasNode.LayerRoot); - Assert.Equal(Brushes.Red, scene.Layers[border].OpacityMask); - - Assert.Equal(2, scene.Layers.Count()); - Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border })); - - border.OpacityMask = null; - scene = scene.Clone(); - - sceneBuilder.Update(scene, border); - - rootNode = (VisualNode)scene.Root; - borderNode = (VisualNode)scene.FindNode(border); - canvasNode = (VisualNode)scene.FindNode(canvas); - - Assert.Same(tree, rootNode.LayerRoot); - Assert.Same(tree, borderNode.LayerRoot); - Assert.Same(tree, canvasNode.LayerRoot); - Assert.Single(scene.Layers); - - var rootDirty = scene.Layers[tree].Dirty; - - Assert.Single(rootDirty); - Assert.Equal(new Rect(21, 21, 58, 78), rootDirty.Single()); - } - } - - [Fact] - public void Removing_Transparent_Control_Should_Remove_Layers() - { - using (TestApplication()) - { - Decorator decorator; - Border border; - Canvas canvas; - var tree = new TestRoot - { - Padding = new Thickness(10), - Width = 100, - Height = 120, - Child = decorator = new Decorator - { - Padding = new Thickness(11), - Child = border = new Border - { - Opacity = 0.5, - Background = Brushes.Red, - Padding = new Thickness(12), - Child = canvas = new Canvas - { - Opacity = 0.75, - }, - } - } - }; - - var layout = AvaloniaLocator.Current.GetService(); - layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -210,13 +147,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Padding = new Thickness(11), Child = border = new Border { - Opacity = 0.5, Background = Brushes.Red, Padding = new Thickness(12), - Child = canvas = new Canvas - { - Opacity = 0.75, - }, + Child = canvas = new Canvas(), } } }; @@ -224,6 +157,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var layout = AvaloniaLocator.Current.GetService(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation); + var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); sceneBuilder.UpdateAll(scene); @@ -263,6 +200,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var layout = AvaloniaLocator.Current.GetService(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); sceneBuilder.UpdateAll(scene); From 9609d93f3b253f5c364b0605df30a025c2c87fe1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Dec 2017 09:25:19 +0100 Subject: [PATCH 04/10] Don't create render layers for non-parent controls. Only create a new render layer when a control has both an animating opacity _and_ children. --- .../Rendering/SceneGraph/SceneBuilder.cs | 11 +++- .../Rendering/DeferredRendererTests.cs | 1 + .../SceneGraph/SceneBuilderTests_Layers.cs | 51 +++++++++++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 6c92ff022c..4f5f094e29 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -167,7 +167,6 @@ namespace Avalonia.Rendering.SceneGraph using (context.PushPostTransform(m)) using (context.PushTransformContainer()) { - var startLayer = (visual as IAvaloniaObject)?.IsAnimating(Visual.OpacityProperty) ?? false; var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); forceRecurse = forceRecurse || @@ -181,7 +180,7 @@ namespace Avalonia.Rendering.SceneGraph node.Opacity = opacity; node.OpacityMask = visual.OpacityMask; - if (startLayer) + if (ShouldStartLayer(visual)) { if (node.LayerRoot != visual) { @@ -366,6 +365,14 @@ namespace Avalonia.Rendering.SceneGraph } } + private static bool ShouldStartLayer(IVisual visual) + { + var o = visual as IAvaloniaObject; + return visual.VisualChildren.Count > 0 && + o != null && + o.IsAnimating(Visual.OpacityProperty); + } + private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) { IGeometryImpl result = null; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 9d90ae23af..afe7b04664 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -261,6 +261,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering Child = border = new Border { Background = Brushes.Green, + Child = new Canvas(), } } }; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index 10372c7d8c..f2d137249a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -15,7 +15,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public partial class SceneBuilderTests { [Fact] - public void Control_With_Animated_Opacity_Should_Start_New_Layer() + public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer() { using (TestApplication()) { @@ -34,7 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { Background = Brushes.Red, Padding = new Thickness(12), - Child = canvas = new Canvas(), + Child = canvas = new Canvas() } } }; @@ -82,6 +82,42 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Control_With_Animated_Opacity_And_No_Children_Should_Not_Start_New_Layer() + { + using (TestApplication()) + { + Decorator decorator; + Border border; + var tree = new TestRoot + { + Padding = new Thickness(10), + Width = 100, + Height = 120, + Child = decorator = new Decorator + { + Padding = new Thickness(11), + Child = border = new Border + { + Background = Brushes.Red, + } + } + }; + + var layout = AvaloniaLocator.Current.GetService(); + layout.ExecuteInitialLayoutPass(tree); + + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + Assert.Single(scene.Layers); + } + } + [Fact] public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers() { @@ -102,7 +138,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { Background = Brushes.Red, Padding = new Thickness(12), - Child = canvas = new Canvas() + Child = canvas = new Canvas + { + Children = { new TextBlock() }, + } } } }; @@ -149,7 +188,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { Background = Brushes.Red, Padding = new Thickness(12), - Child = canvas = new Canvas(), + Child = canvas = new Canvas + { + Children = { new TextBlock() }, + } } } }; @@ -193,6 +235,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Child = border = new Border { Opacity = 0.5, + Child = new Canvas(), } } }; From d0be429bd29a73027eb7782980fdfa47f4906529 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Dec 2017 22:39:59 +0100 Subject: [PATCH 05/10] Tried to make IsAnimating logic clearer. --- src/Avalonia.Base/PriorityValue.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index e1b62c030c..12a9e20528 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -52,7 +52,17 @@ namespace Avalonia _validate = validate; } - public bool IsAnimating => ValuePriority < 0 && GetLevel(ValuePriority).ActiveBindingIndex != -1; + /// + /// Gets a value indicating whether the property is animating. + /// + public bool IsAnimating + { + get + { + return ValuePriority <= (int)BindingPriority.Animation && + GetLevel(ValuePriority).ActiveBindingIndex != -1; + } + } /// /// Gets the owner of the value. From ea0e4217a7695c3cd3b67e5be1ffead2b4146d14 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 Dec 2017 09:12:43 +0100 Subject: [PATCH 06/10] Run D2D render tests in ncrunch. --- .ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject index a8c3abe8f2..04ab17c4e1 100644 --- a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject @@ -1,7 +1,6 @@  - 1000 - True + 3000 True \ No newline at end of file From e6b5df8d089aaef984627fa43ecc61402f91eb8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 Dec 2017 10:21:52 +0100 Subject: [PATCH 07/10] Simplified DeferredRendererTests, --- .../Rendering/DeferredRenderer.cs | 18 ++- tests/Avalonia.UnitTests/TestRoot.cs | 18 ++- .../Rendering/DeferredRendererTests.cs | 140 +++++------------- 3 files changed, 63 insertions(+), 113 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index cba09297f3..2bc7121d73 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -25,7 +25,6 @@ namespace Avalonia.Rendering private readonly IRenderLoop _renderLoop; private readonly IVisual _root; private readonly ISceneBuilder _sceneBuilder; - private readonly RenderLayers _layers; private bool _running; private Scene _scene; @@ -56,7 +55,7 @@ namespace Avalonia.Rendering _dispatcher = dispatcher ?? Dispatcher.UIThread; _root = root; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); _renderLoop = renderLoop; } @@ -80,7 +79,7 @@ namespace Avalonia.Rendering _root = root; _renderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); } /// @@ -94,6 +93,11 @@ namespace Avalonia.Rendering /// public string DebugFramesPath { get; set; } + /// + /// Gets the render layers. + /// + internal RenderLayers Layers { get; } + /// public void AddDirty(IVisual visual) { @@ -192,7 +196,7 @@ namespace Avalonia.Rendering if (scene.Generation != _lastSceneId) { context = _renderTarget.CreateDrawingContext(this); - _layers.Update(scene, context); + Layers.Update(scene, context); RenderToLayers(scene); @@ -262,7 +266,7 @@ namespace Avalonia.Rendering { foreach (var layer in scene.Layers) { - var renderTarget = _layers[layer.LayerRoot].Bitmap; + var renderTarget = Layers[layer.LayerRoot].Bitmap; var node = (VisualNode)scene.FindNode(layer.LayerRoot); if (node != null) @@ -322,7 +326,7 @@ namespace Avalonia.Rendering foreach (var layer in scene.Layers) { - var bitmap = _layers[layer.LayerRoot].Bitmap; + var bitmap = Layers[layer.LayerRoot].Bitmap; var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); if (layer.GeometryClip != null) @@ -442,7 +446,7 @@ namespace Avalonia.Rendering { var index = 0; - foreach (var layer in _layers) + foreach (var layer in Layers) { var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png"); layer.Bitmap.Save(fileName); diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 9ec053f075..dc137e3533 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -16,8 +16,6 @@ namespace Avalonia.UnitTests public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, INameScope, IRenderRoot, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); - private readonly IRenderTarget _renderTarget = Mock.Of( - x => x.CreateDrawingContext(It.IsAny()) == Mock.Of()); public TestRoot() { @@ -65,7 +63,21 @@ namespace Avalonia.UnitTests IStyleHost IStyleHost.StylingParent => StylingParent; - public IRenderTarget CreateRenderTarget() => _renderTarget; + public IRenderTarget CreateRenderTarget() + { + var dc = new Mock(); + dc.Setup(x => x.CreateLayer(It.IsAny())).Returns(() => + { + var layerDc = new Mock(); + var layer = new Mock(); + layer.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(layerDc.Object); + return layer.Object; + }); + + var result = new Mock(); + result.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(dc.Object); + return result.Object; + } public void Invalidate(Rect rect) { diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index afe7b04664..e3b2577e79 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -21,28 +21,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void First_Frame_Calls_UpdateScene_On_Dispatcher() { - var loop = new Mock(); var root = new TestRoot(); var dispatcher = new Mock(); dispatcher.Setup(x => x.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) .Callback((a, p) => a()); - var target = new DeferredRenderer( - root, - loop.Object, - sceneBuilder: MockSceneBuilder(root).Object, - dispatcher: dispatcher.Object); + CreateTargetAndRunFrame(root, dispatcher: dispatcher.Object); - target.Start(); - RunFrame(loop); - -#if !NETCOREAPP1_1 // Delegate.Method is not available in netcoreapp1.1 dispatcher.Verify(x => x.InvokeAsync( It.Is(a => a.Method.Name == "UpdateScene"), DispatcherPriority.Render)); -#endif } [Fact] @@ -51,15 +41,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); - var dispatcher = new ImmediateDispatcher(); - var target = new DeferredRenderer( - root, - loop.Object, - sceneBuilder: sceneBuilder.Object, - dispatcher: dispatcher); - target.Start(); - RunFrame(loop); + CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object); sceneBuilder.Verify(x => x.UpdateAll(It.IsAny())); } @@ -70,12 +53,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); - var dispatcher = new ImmediateDispatcher(); var target = new DeferredRenderer( root, loop.Object, - sceneBuilder: sceneBuilder.Object, - dispatcher: dispatcher); + sceneBuilder: sceneBuilder.Object); target.Start(); IgnoreFirstFrame(loop, sceneBuilder); @@ -145,24 +126,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var rootLayer = CreateLayer(); - var borderLayer = CreateLayer(); - var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); - renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny())) - .Returns(rootLayer) - .Returns(borderLayer); - - var loop = new Mock(); - var target = new DeferredRenderer( - root, - loop.Object, - dispatcher: new ImmediateDispatcher()); - root.Renderer = target; - - target.Start(); - RunFrame(loop); - - var context = Mock.Get(rootLayer.CreateDrawingContext(null)); + var target = CreateTargetAndRunFrame(root); + var context = GetLayerContext(target, root); var animation = new BehaviorSubject(0.5); context.Verify(x => x.PushOpacity(0.5), Times.Once); @@ -191,24 +156,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var rootLayer = CreateLayer(); - var borderLayer = CreateLayer(); - var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); - renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny())) - .Returns(rootLayer) - .Returns(borderLayer); - - var loop = new Mock(); - var target = new DeferredRenderer( - root, - loop.Object, - dispatcher: new ImmediateDispatcher()); - root.Renderer = target; - - target.Start(); - RunFrame(loop); - - var context = Mock.Get(rootLayer.CreateDrawingContext(null)); + var target = CreateTargetAndRunFrame(root); + var context = GetLayerContext(target, root); var animation = new BehaviorSubject(0.5); context.Verify(x => x.PushOpacity(0.5), Times.Never); @@ -217,12 +166,11 @@ namespace Avalonia.Visuals.UnitTests.Rendering } [Fact] - public void Frame_Should_Create_Layer_For_Root() + public void Should_Create_Layer_For_Root() { var loop = new Mock(); var root = new TestRoot(); var rootLayer = new Mock(); - var dispatcher = new ImmediateDispatcher(); var sceneBuilder = new Mock(); sceneBuilder.Setup(x => x.UpdateAll(It.IsAny())) @@ -233,18 +181,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering }); var renderInterface = new Mock(); + var target = CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object); - var target = new DeferredRenderer( - root, - loop.Object, - sceneBuilder: sceneBuilder.Object, - dispatcher: dispatcher); - - target.Start(); - RunFrame(loop); - - var context = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); - context.Verify(x => x.CreateLayer(root.ClientSize)); + Assert.Single(target.Layers); } [Fact] @@ -269,49 +208,44 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var rootLayer = CreateLayer(); - var borderLayer = CreateLayer(); - var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); - renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny())) - .Returns(rootLayer) - .Returns(borderLayer); - var loop = new Mock(); - var target = new DeferredRenderer( - root, - loop.Object, - dispatcher: new ImmediateDispatcher()); - root.Renderer = target; + var target = CreateTargetAndRunFrame(root, loop: loop); - target.Start(); - RunFrame(loop); + Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); - var rootContext = Mock.Get(rootLayer.CreateDrawingContext(null)); - var borderContext = Mock.Get(borderLayer.CreateDrawingContext(null)); var animation = new BehaviorSubject(0.5); - - 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(), It.IsAny(), It.IsAny()), Times.Never); - - rootContext.ResetCalls(); - borderContext.ResetCalls(); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); 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); + Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot)); - rootContext.ResetCalls(); - borderContext.ResetCalls(); animation.OnCompleted(); RunFrame(loop); - Mock.Get(borderLayer).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(), It.IsAny(), It.IsAny()), Times.Never); + Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); + } + + private DeferredRenderer CreateTargetAndRunFrame( + TestRoot root, + Mock loop = null, + ISceneBuilder sceneBuilder = null, + IDispatcher dispatcher = null) + { + loop = loop ?? new Mock(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher ?? new ImmediateDispatcher()); + root.Renderer = target; + target.Start(); + RunFrame(loop); + return target; + } + + private Mock GetLayerContext(DeferredRenderer renderer, IControl layerRoot) + { + return Mock.Get(renderer.Layers[layerRoot].Bitmap.CreateDrawingContext(null)); } private void IgnoreFirstFrame(Mock loop, Mock sceneBuilder) From 1d1f31794e56f5ed54b83209a43eb5b9678b9fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 Dec 2017 10:41:38 +0100 Subject: [PATCH 08/10] Fix layers with opacity. We were pushing the opacity twice. --- .../Rendering/DeferredRenderer.cs | 28 +++--- .../Rendering/SceneGraph/IVisualNode.cs | 6 +- .../Rendering/SceneGraph/VisualNode.cs | 10 +-- .../Rendering/DeferredRendererTests.cs | 88 +++++++++++++++++++ 4 files changed, 114 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 2bc7121d73..041d8f8f6b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -28,7 +28,6 @@ namespace Avalonia.Rendering private bool _running; private Scene _scene; - private IRenderTarget _renderTarget; private DirtyVisuals _dirty; private IRenderTargetBitmapImpl _overlay; private bool _updateQueued; @@ -77,7 +76,7 @@ namespace Avalonia.Rendering Contract.Requires(renderTarget != null); _root = root; - _renderTarget = renderTarget; + RenderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); } @@ -98,6 +97,11 @@ namespace Avalonia.Rendering /// internal RenderLayers Layers { get; } + /// + /// Gets the current render target. + /// + internal IRenderTarget RenderTarget { get; private set; } + /// public void AddDirty(IVisual visual) { @@ -177,9 +181,9 @@ namespace Avalonia.Rendering bool renderOverlay = DrawDirtyRects || DrawFps; bool composite = false; - if (_renderTarget == null) + if (RenderTarget == null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); } if (renderOverlay) @@ -195,7 +199,7 @@ namespace Avalonia.Rendering if (scene.Generation != _lastSceneId) { - context = _renderTarget.CreateDrawingContext(this); + context = RenderTarget.CreateDrawingContext(this); Layers.Update(scene, context); RenderToLayers(scene); @@ -212,13 +216,13 @@ namespace Avalonia.Rendering if (renderOverlay) { - context = context ?? _renderTarget.CreateDrawingContext(this); + context = context ?? RenderTarget.CreateDrawingContext(this); RenderOverlay(scene, context); RenderComposite(scene, context); } else if (composite) { - context = context ?? _renderTarget.CreateDrawingContext(this); + context = context ?? RenderTarget.CreateDrawingContext(this); RenderComposite(scene, context); } @@ -228,8 +232,8 @@ namespace Avalonia.Rendering catch (RenderTargetCorruptedException ex) { Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget?.Dispose(); - _renderTarget = null; + RenderTarget?.Dispose(); + RenderTarget = null; } } @@ -241,7 +245,9 @@ namespace Avalonia.Rendering if (!clipBounds.IsEmpty && node.Opacity > 0) { - node.BeginRender(context); + var isLayerRoot = node.Visual == layer; + + node.BeginRender(context, isLayerRoot); foreach (var operation in node.DrawOperations) { @@ -255,7 +261,7 @@ namespace Avalonia.Rendering Render(context, (VisualNode)child, layer, clipBounds); } - node.EndRender(context); + node.EndRender(context, isLayerRoot); } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 0d2fc17b95..234cadbf31 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -72,13 +72,15 @@ namespace Avalonia.Rendering.SceneGraph /// Sets up the drawing context for rendering the node's geometry. /// /// The drawing context. - void BeginRender(IDrawingContextImpl context); + /// Whether to skip pushing the control's opacity. + void BeginRender(IDrawingContextImpl context, bool skipOpacity); /// /// Resets the drawing context after rendering the node's geometry. /// /// The drawing context. - void EndRender(IDrawingContextImpl context); + /// Whether to skip popping the control's opacity. + void EndRender(IDrawingContextImpl context, bool skipOpacity); /// /// Hit test the geometry in this node. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index ea3b07ffe3..e9e20557bd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -218,15 +218,15 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void BeginRender(IDrawingContextImpl context) + public void BeginRender(IDrawingContextImpl context, bool skipOpacity) { if (ClipToBounds) { context.Transform = Matrix.Identity; context.PushClip(ClipBounds); } - - if (Opacity != 1) + + if (Opacity != 1 && !skipOpacity) { context.PushOpacity(Opacity); } @@ -240,14 +240,14 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void EndRender(IDrawingContextImpl context) + public void EndRender(IDrawingContextImpl context, bool skipOpacity) { if (GeometryClip != null) { context.PopGeometryClip(); } - if (Opacity != 1) + if (Opacity != 1 && !skipOpacity) { context.PopOpacity(); } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index e3b2577e79..21065cb6b5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -201,6 +201,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering { Background = Brushes.Green, Child = new Canvas(), + Opacity = 0.9, } } }; @@ -225,6 +226,93 @@ namespace Avalonia.Visuals.UnitTests.Rendering Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); } + [Fact] + public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity() + { + Border border; + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Border + { + Background = Brushes.Red, + Child = border = new Border + { + Background = Brushes.Green, + } + } + }; + + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var loop = new Mock(); + var target = CreateTargetAndRunFrame(root, loop: loop); + + Assert.Single(target.Layers); + } + + [Fact] + public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control() + { + Border border; + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = border = new Border + { + Background = Brushes.Red, + Child = new Canvas(), + } + }; + + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var target = CreateTargetAndRunFrame(root); + var context = GetLayerContext(target, border); + + context.Verify(x => x.PushOpacity(0.5), Times.Never); + context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); + context.Verify(x => x.PopOpacity(), Times.Never); + } + + [Fact] + public void Should_Draw_Transparent_Layer_With_Correct_Opacity() + { + Border border; + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = border = new Border + { + Background = Brushes.Red, + Child = new Canvas(), + } + }; + + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var target = CreateTargetAndRunFrame(root); + var context = Mock.Get(target.RenderTarget.CreateDrawingContext(null)); + var borderLayer = target.Layers[border].Bitmap; + + context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny(), It.IsAny())); + } + private DeferredRenderer CreateTargetAndRunFrame( TestRoot root, Mock loop = null, From 2a87dbd6caf4e90bef242fb49562e494546d048f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 Dec 2017 10:53:55 +0100 Subject: [PATCH 09/10] Fix opacity mask rendering. We're potentially creating a new immutable version of the opacity mask each time the `VisualNode` is updated. Need to implement comparison between mutable and immutable brushes. --- .../Rendering/SceneGraph/SceneBuilder.cs | 4 ++- .../Rendering/SceneGraph/VisualNode.cs | 10 +++++++ .../Rendering/DeferredRendererTests.cs | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 4f5f094e29..8f4f487e08 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -178,7 +178,9 @@ namespace Avalonia.Rendering.SceneGraph node.ClipToBounds = clipToBounds; node.GeometryClip = visual.Clip?.PlatformImpl; node.Opacity = opacity; - node.OpacityMask = visual.OpacityMask; + + // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. + node.OpacityMask = visual.OpacityMask?.ToImmutable(); if (ShouldStartLayer(visual)) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index e9e20557bd..ecff1a1077 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -231,6 +231,11 @@ namespace Avalonia.Rendering.SceneGraph context.PushOpacity(Opacity); } + if (OpacityMask != null) + { + context.PushOpacityMask(OpacityMask, ClipBounds); + } + context.Transform = Transform; if (GeometryClip != null) @@ -247,6 +252,11 @@ namespace Avalonia.Rendering.SceneGraph context.PopGeometryClip(); } + if (OpacityMask != null) + { + context.PopOpacityMask(); + } + if (Opacity != 1 && !skipOpacity) { context.PopOpacity(); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 21065cb6b5..c97070a2aa 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -165,6 +165,32 @@ namespace Avalonia.Visuals.UnitTests.Rendering context.Verify(x => x.PopOpacity(), Times.Never); } + [Fact] + public void Should_Push_Opacity_Mask() + { + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Border + { + Background = Brushes.Red, + OpacityMask = Brushes.Green, + } + }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var target = CreateTargetAndRunFrame(root); + var context = GetLayerContext(target, root); + var animation = new BehaviorSubject(0.5); + + context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once); + context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); + context.Verify(x => x.PopOpacityMask(), Times.Once); + } + [Fact] public void Should_Create_Layer_For_Root() { From b3fca2360de4fa933c2bf3a7cf650659e3876794 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Dec 2017 02:10:58 +0100 Subject: [PATCH 10/10] Fix failing skia render test Make sure we restore the transform in `EndRender` and use the same order of pushing/popping operations that `ImmediateRenderer` uses. --- .../Rendering/SceneGraph/VisualNode.cs | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index ecff1a1077..6bea4d9bd6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -22,6 +22,7 @@ namespace Avalonia.Rendering.SceneGraph private List _children; private List _drawOperations; private bool _drawOperationsCloned; + private Matrix transformRestore; /// /// Initializes a new instance of the class. @@ -220,41 +221,43 @@ namespace Avalonia.Rendering.SceneGraph /// public void BeginRender(IDrawingContextImpl context, bool skipOpacity) { + transformRestore = context.Transform; + if (ClipToBounds) { context.Transform = Matrix.Identity; context.PushClip(ClipBounds); } - + + context.Transform = Transform; + if (Opacity != 1 && !skipOpacity) { context.PushOpacity(Opacity); } - if (OpacityMask != null) + if (GeometryClip != null) { - context.PushOpacityMask(OpacityMask, ClipBounds); + context.PushGeometryClip(GeometryClip); } - context.Transform = Transform; - - if (GeometryClip != null) + if (OpacityMask != null) { - context.PushGeometryClip(GeometryClip); + context.PushOpacityMask(OpacityMask, ClipBounds); } } /// public void EndRender(IDrawingContextImpl context, bool skipOpacity) { - if (GeometryClip != null) + if (OpacityMask != null) { - context.PopGeometryClip(); + context.PopOpacityMask(); } - if (OpacityMask != null) + if (GeometryClip != null) { - context.PopOpacityMask(); + context.PopGeometryClip(); } if (Opacity != 1 && !skipOpacity) @@ -264,8 +267,11 @@ namespace Avalonia.Rendering.SceneGraph if (ClipToBounds) { + context.Transform = Matrix.Identity; context.PopClip(); } + + context.Transform = transformRestore; } private Rect CalculateBounds()