Selaa lähdekoodia

Initial impl of incremental update to SceneGraph.

Steven Kirk 9 vuotta sitten
vanhempi
sitoutus
934e18c8ba

+ 2 - 0
src/Avalonia.Base/Threading/Dispatcher.cs

@@ -79,6 +79,8 @@ namespace Avalonia.Threading
         /// </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.
         public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
         {
             _jobRunner?.Post(action, priority);

+ 56 - 34
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Diagnostics;
-using System.Linq;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
@@ -16,6 +15,7 @@ namespace Avalonia.Rendering
         private readonly IRenderRoot _root;
         private Scene _scene;
         private IRenderTarget _renderTarget;
+        private List<IVisual> _dirty = new List<IVisual>();
         private bool _needsUpdate;
         private bool _needsRender;
 
@@ -39,11 +39,14 @@ namespace Avalonia.Rendering
 
         public void AddDirty(IVisual visual)
         {
-            if (!_needsUpdate)
+            // If the root of the scene has no children, then the scene is being set up; don't
+            // bother filling the dirty list with every control in the window.
+            if (_scene.Root.Children.Count > 0)
             {
-                _needsUpdate = true;
-                Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
+                _dirty.Add(visual);
             }
+
+            _needsUpdate = true;
         }
 
         public void Dispose()
@@ -63,31 +66,6 @@ namespace Avalonia.Rendering
 
         public void Render(Rect rect)
         {
-            if (_renderTarget == null)
-            {
-                _renderTarget = _root.CreateRenderTarget();
-            }
-
-            try
-            {
-                _totalFrames++;
-
-                using (var context = _renderTarget.CreateDrawingContext())
-                {
-                    _scene.Root.Render(context);
-
-                    if (DrawFps)
-                    {
-                        RenderFps(context);
-                    }
-                }
-            }
-            catch (RenderTargetCorruptedException ex)
-            {
-                Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
-                _renderTarget.Dispose();
-                _renderTarget = null;
-            }
         }
 
         private void RenderFps(IDrawingContextImpl context)
@@ -122,7 +100,22 @@ namespace Avalonia.Rendering
         {
             Dispatcher.UIThread.VerifyAccess();
 
-            _scene = SceneBuilder.Update(_scene);
+            var scene = _scene.Clone();
+
+            if (_dirty.Count > 0)
+            {
+                foreach (var visual in _dirty)
+                {
+                    SceneBuilder.Update(scene, visual);
+                }
+            }
+            else
+            {
+                SceneBuilder.UpdateAll(scene);
+            }
+
+            _scene = scene;
+
             _needsUpdate = false;
             _needsRender = true;
             _root.Invalidate(new Rect(_root.ClientSize));
@@ -130,10 +123,39 @@ namespace Avalonia.Rendering
 
         private void OnRenderLoopTick(object sender, EventArgs e)
         {
-            //if (_needsRender)
-            //{
-            //    _needsRender = false;
-            //}
+            if (_needsUpdate)
+            {
+                Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
+            }
+
+            if (_needsRender)
+            {
+                if (_renderTarget == null)
+                {
+                    _renderTarget = _root.CreateRenderTarget();
+                }
+
+                try
+                {
+                    _totalFrames++;
+
+                    using (var context = _renderTarget.CreateDrawingContext())
+                    {
+                        _scene.Root.Render(context);
+
+                        if (DrawFps)
+                        {
+                            RenderFps(context);
+                        }
+                    }
+                }
+                catch (RenderTargetCorruptedException ex)
+                {
+                    Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
+                    _renderTarget.Dispose();
+                    _renderTarget = null;
+                }
+            }
         }
     }
 }

+ 23 - 23
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Reactive.Disposables;
 using Avalonia.Media;
 using Avalonia.Platform;
@@ -25,30 +26,39 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         public IDisposable Begin(VisualNode node)
-        {
-            _stack.Push(new Frame(node));
-            return Disposable.Create(Pop);
-        }
-
-        public void Dispose()
-        {
-        }
-
-        public void AddChild(IVisualNode visualNode)
         {
             if (_stack.Count > 0)
             {
                 var next = NextNodeAs<VisualNode>();
 
-                if (next == null || next != visualNode)
+                if (next == null || next != node)
                 {
-                    Add(visualNode);
+                    Add(node);
                 }
                 else
                 {
                     ++Index;
                 }
             }
+
+            _stack.Push(new Frame(node));
+            return Disposable.Create(Pop);
+        }
+
+        public void Dispose()
+        {
+        }
+
+        public void TrimNodes()
+        {
+            var frame = _stack.Peek();
+            var children = frame.Node.Children;
+            var index = frame.Index;
+
+            if (children.Count > index)
+            {
+                children.RemoveRange(index, children.Count - index);
+            }
         }
 
         public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
@@ -196,17 +206,7 @@ namespace Avalonia.Rendering.SceneGraph
             return Index < Node.Children.Count ? Node.Children[Index] as T : null;
         }
 
-        private void Pop()
-        {
-            var frame = _stack.Pop();
-            var children = frame.Node.Children;
-            var index = frame.Index;
-
-            if (children.Count > index)
-            {
-                children.RemoveRange(index, children.Count - index);
-            }
-        }
+        private void Pop() => _stack.Pop();
 
         class Frame
         {

+ 4 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@@ -10,8 +10,10 @@ namespace Avalonia.Rendering.SceneGraph
     public interface IVisualNode : ISceneNode
     {
         IVisual Visual { get; }
-        Rect ClipBounds { get; set; }
-        bool ClipToBounds { get; set; }
+        IVisualNode Parent { get; }
+        Matrix Transform { get; }
+        Rect ClipBounds { get; }
+        bool ClipToBounds { get; }
         IReadOnlyList<ISceneNode> Children { get; }
 
         bool HitTest(Point p);

+ 13 - 5
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -12,8 +12,9 @@ namespace Avalonia.Rendering.SceneGraph
         private Dictionary<IVisual, IVisualNode> _index;
 
         public Scene(IVisual rootVisual)
-            : this(new VisualNode(rootVisual), new Dictionary<IVisual, IVisualNode>())
+            : this(new VisualNode(rootVisual, null), new Dictionary<IVisual, IVisualNode>())
         {
+            _index.Add(rootVisual, Root);
         }
 
         internal Scene(VisualNode root, Dictionary<IVisual, IVisualNode> index)
@@ -28,6 +29,8 @@ namespace Avalonia.Rendering.SceneGraph
 
         public void Add(IVisualNode node)
         {
+            Contract.Requires<ArgumentNullException>(node != null);
+
             _index.Add(node.Visual, node);
         }
 
@@ -51,9 +54,16 @@ namespace Avalonia.Rendering.SceneGraph
             return HitTest(Root, p, null, filter);
         }
 
-        private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary<IVisual, IVisualNode> index)
+        public void Remove(IVisualNode node)
+        {
+            Contract.Requires<ArgumentNullException>(node != null);
+
+            _index.Remove(node.Visual);
+        }
+
+        private VisualNode Clone(VisualNode source, IVisualNode parent, Dictionary<IVisual, IVisualNode> index)
         {
-            var result = source.Clone();
+            var result = source.Clone(parent);
 
             index.Add(result.Visual, result);
 
@@ -99,8 +109,6 @@ namespace Avalonia.Rendering.SceneGraph
                         }
                     }
 
-                    dynamic d = node.Visual;
-
                     if (node.HitTest(p))
                     {
                         yield return node.Visual;

+ 121 - 14
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -11,32 +11,113 @@ namespace Avalonia.Rendering.SceneGraph
 {
     public static class SceneBuilder
     {
-        public static Scene Update(Scene scene)
+        public static void UpdateAll(Scene scene)
         {
             Dispatcher.UIThread.VerifyAccess();
 
-            scene = scene.Clone();
-
             using (var impl = new DeferredDrawingContextImpl())
             using (var context = new DrawingContext(impl))
             {
-                Update(context, scene, scene.Root.Visual, null);
+                Update(context, scene, (VisualNode)scene.Root, true);
             }
+        }
+
+        public static bool Update(Scene scene, IVisual visual)
+        {
+            Dispatcher.UIThread.VerifyAccess();
+
+            var node = (VisualNode)scene.FindNode(visual);
+
+            if (visual.VisualRoot != null)
+            {
+                if (visual.IsVisible)
+                {
+                    // If the node isn't yet part of the scene, find the nearest ancestor that is.
+                    node = node ?? FindExistingAncestor(scene, visual);
+
+                    // We don't need to do anything if this part of the tree has already been fully
+                    // updated.
+                    if (node != null && !node.SubTreeUpdated)
+                    {
+                        // If the control we've been asked to update isn't part of the scene then
+                        // we're carrying out an add operation, so recurse and add all the
+                        // descendents too.
+                        var recurse = node.Visual != visual;
+
+                        using (var impl = new DeferredDrawingContextImpl())
+                        using (var context = new DrawingContext(impl))
+                        {
+                            if (node.Parent != null)
+                            {
+                                context.PushPostTransform(node.Parent.Transform);
+                            }
+
+                            Update(context, scene, (VisualNode)node, recurse);
+                        }
 
-            return scene;
+                        return true;
+                    }
+                }
+                else
+                {
+                    if (node != null)
+                    {
+                        // The control has been removed so remove it from its parent and deindex the
+                        // node and its descendents.
+                        ((VisualNode)node.Parent)?.Children.Remove(node);
+                        Deindex(scene, node);
+                        return true;
+                    }
+                }
+            }
+            else if (node != null)
+            {
+                // The control has been removed so remove it from its parent and deindex the
+                // node and its descendents.
+                var trim = FindFirstDeadAncestor(scene, node);
+                ((VisualNode)trim.Parent).Children.Remove(trim);
+                Deindex(scene, trim);
+                return true;
+            }
+
+            return false;
         }
 
-        private static void Update(DrawingContext context, Scene scene, IVisual visual, VisualNode parent)
+        private static VisualNode FindExistingAncestor(Scene scene, IVisual visual)
         {
+            var node = scene.FindNode(visual);
+
+            while (node == null && visual.IsVisible)
+            {
+                visual = visual.VisualParent;
+                node = scene.FindNode(visual);
+            }
+
+            return visual.IsVisible ? (VisualNode)node : null;
+        }
+
+        private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node)
+        {
+            var parent = node.Parent;
+
+            while (parent.Visual.VisualRoot == null)
+            {
+                node = parent;
+                parent = node.Parent;
+            }
+
+            return (VisualNode)node;
+        }
+
+        private static void Update(DrawingContext context, Scene scene, VisualNode node, bool forceRecurse)
+        {
+            var visual = node.Visual;
             var opacity = visual.Opacity;
             var clipToBounds = visual.ClipToBounds;
             var bounds = new Rect(visual.Bounds.Size);
-            var node = (VisualNode)scene.FindNode(visual) ?? CreateNode(visual, scene, parent);
             var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
 
-            contextImpl.AddChild(node);
-
-            if (visual.IsVisible && opacity > 0)
+            if (visual.IsVisible)
             {
                 var m = Matrix.CreateTranslation(visual.Bounds.Position);
 
@@ -55,6 +136,9 @@ namespace Avalonia.Rendering.SceneGraph
                 using (context.PushPostTransform(m))
                 using (context.PushTransformContainer())
                 {
+                    forceRecurse = forceRecurse ||
+                        node.Transform != contextImpl.Transform;
+
                     node.Transform = contextImpl.Transform;
                     node.ClipBounds = bounds * node.Transform;
                     node.ClipToBounds = clipToBounds;
@@ -64,19 +148,42 @@ namespace Avalonia.Rendering.SceneGraph
 
                     visual.Render(context);
 
-                    foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
+                    if (forceRecurse)
                     {
-                        Update(context, scene, child, node);
+                        foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
+                        {
+                            var childNode = scene.FindNode(child) ?? CreateNode(scene, child, node);
+                            Update(context, scene, (VisualNode)childNode, forceRecurse);
+                        }
+
+                        node.SubTreeUpdated = true;
+                        contextImpl.TrimNodes();
                     }
                 }
             }
         }
 
-        private static VisualNode CreateNode(IVisual visual, Scene scene, VisualNode parent)
+        private static VisualNode CreateNode(Scene scene, IVisual visual, IVisualNode parent)
         {
-            var node = new VisualNode(visual);
+            var node = new VisualNode(visual, parent);
             scene.Add(node);
             return node;
         }
+
+        private static void Deindex(Scene scene, VisualNode node)
+        {
+            scene.Remove(node);
+            node.SubTreeUpdated = true;
+
+            foreach (var child in node.Children)
+            {
+                var visualChild = child as VisualNode;
+
+                if (visualChild != null)
+                {
+                    Deindex(scene, visualChild);
+                }
+            }
+        }
     }
 }

+ 21 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -10,13 +10,21 @@ namespace Avalonia.Rendering.SceneGraph
 {
     public class VisualNode : IVisualNode
     {
-        public VisualNode(IVisual visual)
+        public VisualNode(IVisual visual, IVisualNode parent)
         {
-            Children = new List<ISceneNode>();
+            if (parent == null && visual.VisualParent != null)
+            {
+                throw new AvaloniaInternalException(
+                    "Attempted to create root VisualNode for parented visual.");
+            }
+
             Visual = visual;
+            Parent = parent;
+            Children = new List<ISceneNode>();
         }
 
         public IVisual Visual { get; }
+        public IVisualNode Parent { get; }
         public Matrix Transform { get; set; }
         public Rect ClipBounds { get; set; }
         public bool ClipToBounds { get; set; }
@@ -24,12 +32,21 @@ namespace Avalonia.Rendering.SceneGraph
         public double Opacity { get; set; }
         public IBrush OpacityMask { get; set; }
         public List<ISceneNode> Children { get; }
+        public bool SubTreeUpdated { get; set; }
 
         IReadOnlyList<ISceneNode> IVisualNode.Children => Children;
 
-        public VisualNode Clone()
+        public VisualNode Clone(IVisualNode parent)
         {
-            return new VisualNode(Visual);
+            return new VisualNode(Visual, parent)
+            {
+                Transform = Transform,
+                ClipBounds = ClipBounds,
+                ClipToBounds = ClipToBounds,
+                GeometryClip = GeometryClip,
+                Opacity = Opacity,
+                OpacityMask = OpacityMask,
+            };
         }
 
         public bool HitTest(Point p)

+ 9 - 10
src/Avalonia.Visuals/Visual.cs

@@ -88,6 +88,7 @@ namespace Avalonia
             AvaloniaProperty.Register<Visual, int>(nameof(ZIndex));
 
         private Rect _bounds;
+        private IRenderRoot _visualRoot;
         private IVisual _visualParent;
 
         /// <summary>
@@ -233,11 +234,7 @@ namespace Avalonia
         /// <summary>
         /// Gets the root of the visual tree, if the control is attached to a visual tree.
         /// </summary>
-        protected IRenderRoot VisualRoot
-        {
-            get;
-            private set;
-        }
+        protected IRenderRoot VisualRoot => _visualRoot ?? (this as IRenderRoot);
 
         /// <summary>
         /// Gets a value indicating whether this scene graph node is attached to a visual root.
@@ -326,7 +323,7 @@ namespace Avalonia
         {
             Logger.Verbose(LogArea.Visual, this, "Attached to visual tree");
 
-            VisualRoot = e.Root;
+            _visualRoot = e.Root;
 
             if (RenderTransform != null)
             {
@@ -334,6 +331,7 @@ namespace Avalonia
             }
 
             OnAttachedToVisualTree(e);
+            InvalidateVisual();
 
             if (VisualChildren != null)
             {
@@ -353,7 +351,7 @@ namespace Avalonia
         {
             Logger.Verbose(LogArea.Visual, this, "Detached from visual tree");
 
-            VisualRoot = null;
+            _visualRoot = null;
 
             if (RenderTransform != null)
             {
@@ -361,6 +359,7 @@ namespace Avalonia
             }
 
             OnDetachedFromVisualTree(e);
+            e.Root?.Renderer?.AddDirty(this);
 
             if (VisualChildren != null)
             {
@@ -501,16 +500,16 @@ namespace Avalonia
             var old = _visualParent;
             _visualParent = value;
 
-            if (VisualRoot != null)
+            if (_visualRoot != null)
             {
-                var e = new VisualTreeAttachmentEventArgs(VisualRoot);
+                var e = new VisualTreeAttachmentEventArgs(old, VisualRoot);
                 OnDetachedFromVisualTreeCore(e);
             }
 
             if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
             {
                 var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
-                var e = new VisualTreeAttachmentEventArgs(root);
+                var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
                 OnAttachedToVisualTreeCore(e);
             }
 

+ 10 - 1
src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Rendering;
+using Avalonia.VisualTree;
 
 namespace Avalonia
 {
@@ -15,14 +16,22 @@ namespace Avalonia
         /// <summary>
         /// Initializes a new instance of the <see cref="VisualTreeAttachmentEventArgs"/> class.
         /// </summary>
+        /// <param name="parent">The parent that the visual is being attached to or detached from.</param>
         /// <param name="root">The root visual.</param>
-        public VisualTreeAttachmentEventArgs(IRenderRoot root)
+        public VisualTreeAttachmentEventArgs(IVisual parent, IRenderRoot root)
         {
+            Contract.Requires<ArgumentNullException>(parent != null);
             Contract.Requires<ArgumentNullException>(root != null);
 
+            Parent = parent;
             Root = root;
         }
 
+        /// <summary>
+        /// Gets the parent that the visual is being attached to or detached from.
+        /// </summary>
+        public IVisual Parent { get; }
+
         /// <summary>
         /// Gets the root of the visual tree that the visual is being attached to or detached from.
         /// </summary>

+ 4 - 1
tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject

@@ -7,7 +7,7 @@
   <AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
   <AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
   <AllowCodeAnalysis>false</AllowCodeAnalysis>
-  <IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
+  <IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
   <RunPreBuildEvents>false</RunPreBuildEvents>
   <RunPostBuildEvents>false</RunPostBuildEvents>
   <PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
@@ -23,5 +23,8 @@
   <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
   <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
   <BuildProcessArchitecture>x86</BuildProcessArchitecture>
+  <IgnoredTests>
+    <AllTestsSelector />
+  </IgnoredTests>
   <HiddenWarnings>AbnormalReferenceResolution</HiddenWarnings>
 </ProjectConfiguration>

+ 206 - 14
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -6,6 +6,7 @@ using Avalonia.Rendering.SceneGraph;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
+using Avalonia.Layout;
 
 namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
 {
@@ -35,10 +36,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Measure(Size.Infinity);
                 tree.Arrange(new Rect(tree.DesiredSize));
 
-                var initial = new Scene(tree);
-                var result = SceneBuilder.Update(initial);
+                var result = new Scene(tree);
+                SceneBuilder.UpdateAll(result);
 
-                Assert.NotSame(initial, result);
                 Assert.Equal(1, result.Root.Children.Count);
 
                 var borderNode = (VisualNode)result.Root.Children[0];
@@ -59,6 +59,41 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             }
         }
 
+        [Fact]
+        public void Should_Respect_Margin_For_ClipBounds()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Canvas canvas;
+                var tree = new TestRoot
+                {
+                    Width = 200,
+                    Height = 300,
+                    Child = new Border
+                    {
+                        Margin = new Thickness(10, 20, 30, 40),
+                        Child = canvas = new Canvas(),
+                    }
+                };
+
+                tree.Measure(Size.Infinity);
+                tree.Arrange(new Rect(tree.DesiredSize));
+
+                var result = new Scene(tree);
+                SceneBuilder.UpdateAll(result);
+
+                var canvasNode = result.FindNode(canvas);
+                Assert.Equal(new Rect(10, 20, 160, 240), canvasNode.ClipBounds);
+
+                // Initial ClipBounds are correct, make sure they're still correct after updating canvas.
+                result = result.Clone();
+                Assert.True(SceneBuilder.Update(result, canvas));
+
+                canvasNode = result.FindNode(canvas);
+                Assert.Equal(new Rect(10, 20, 160, 240), canvasNode.ClipBounds);
+            }
+        }
+
         [Fact]
         public void Should_Respect_ZIndex()
         {
@@ -84,7 +119,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                     }
                 };
 
-                var result = SceneBuilder.Update(new Scene(tree));
+                var result = new Scene(tree);
+                SceneBuilder.UpdateAll(result);
 
                 var panelNode = result.FindNode(tree.Child);
                 var expected = new IVisual[] { back, front };
@@ -116,7 +152,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Measure(Size.Infinity);
                 tree.Arrange(new Rect(tree.DesiredSize));
 
-                var result = SceneBuilder.Update(new Scene(tree));
+                var result = new Scene(tree);
+                SceneBuilder.UpdateAll(result);
+
                 var targetNode = result.FindNode(target);
 
                 Assert.Equal(new Rect(50, 50, 100, 100), targetNode.ClipBounds);
@@ -148,7 +186,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Measure(Size.Infinity);
                 tree.Arrange(new Rect(tree.DesiredSize));
 
-                var initial = SceneBuilder.Update(new Scene(tree));
+                var initial = new Scene(tree);
+                SceneBuilder.UpdateAll(initial);
+
                 var initialBackgroundNode = initial.FindNode(border).Children[0];
                 var initialTextNode = initial.FindNode(textBlock).Children[0];
 
@@ -156,9 +196,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 Assert.NotNull(initialTextNode);
 
                 border.Background = Brushes.Green;
-                var result = SceneBuilder.Update(initial);
 
-                Assert.NotSame(initial, result);
+                var result = initial.Clone();
+                SceneBuilder.Update(result, border);
+                
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Same(border, borderNode.Visual);
 
@@ -174,13 +215,64 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             }
         }
 
+        [Fact]
+        public void Should_Update_When_Control_Added()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Border border;
+                var tree = new TestRoot
+                {
+                    Width = 100,
+                    Height = 100,
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Red,
+                    }
+                };
+
+                Canvas canvas;
+                var decorator = new Decorator
+                {
+                    Child = canvas = new Canvas(),
+                };
+
+                tree.Measure(Size.Infinity);
+                tree.Arrange(new Rect(tree.DesiredSize));
+
+                var initial = new Scene(tree);
+                SceneBuilder.UpdateAll(initial);
+
+                border.Child = decorator;
+                var result = initial.Clone();
+
+                Assert.True(SceneBuilder.Update(result, decorator));
+
+                // Updating canvas should result in no-op as it should have been updated along 
+                // with decorator as part of the add opeation.
+                Assert.False(SceneBuilder.Update(result, canvas));
+
+                var borderNode = (VisualNode)result.Root.Children[0];
+                Assert.Equal(2, borderNode.Children.Count);
+
+                var decoratorNode = (VisualNode)borderNode.Children[1];
+                Assert.Same(decorator, decoratorNode.Visual);
+                Assert.Same(decoratorNode, result.FindNode(decorator));
+
+                var canvasNode = (VisualNode)decoratorNode.Children[0];
+                Assert.Same(canvas, canvasNode.Visual);
+                Assert.Same(canvasNode, result.FindNode(canvas));
+            }
+        }
+
         [Fact]
         public void Should_Update_When_Control_Removed()
         {
             using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             {
                 Border border;
-                TextBlock textBlock;
+                Decorator decorator;
+                Canvas canvas;
                 var tree = new TestRoot
                 {
                     Width = 100,
@@ -188,9 +280,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                     Child = border = new Border
                     {
                         Background = Brushes.Red,
-                        Child = textBlock = new TextBlock
+                        Child = decorator = new Decorator
                         {
-                            Text = "Hello World",
+                            Child = canvas = new Canvas()
                         }
                     }
                 };
@@ -198,15 +290,115 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Measure(Size.Infinity);
                 tree.Arrange(new Rect(tree.DesiredSize));
 
-                var initial = SceneBuilder.Update(new Scene(tree));
+                var initial = new Scene(tree);
+                SceneBuilder.UpdateAll(initial);
 
                 border.Child = null;
-                var result = SceneBuilder.Update(initial);
+                var result = initial.Clone();
+
+                Assert.True(SceneBuilder.Update(result, decorator));
+                Assert.False(SceneBuilder.Update(result, canvas));
 
-                Assert.NotSame(initial, result);
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Equal(1, borderNode.Children.Count);
+
+                Assert.Null(result.FindNode(decorator));
+            }
+        }
+
+        [Fact]
+        public void Should_Update_When_Control_Made_Invisible()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Decorator decorator;
+                Border border;
+                Canvas canvas;
+                var tree = new TestRoot
+                {
+                    Width = 100,
+                    Height = 100,
+                    Child = decorator = new Decorator
+                    {
+                        Child = border = new Border
+                        {
+                            Background = Brushes.Red,
+                            Child = canvas = new Canvas(),
+                        }
+                    }
+                };
+
+                tree.Measure(Size.Infinity);
+                tree.Arrange(new Rect(tree.DesiredSize));
+
+                var initial = new Scene(tree);
+                SceneBuilder.UpdateAll(initial);
+
+                border.IsVisible = false;
+                var result = initial.Clone();
+
+                Assert.True(SceneBuilder.Update(result, border));
+                Assert.False(SceneBuilder.Update(result, canvas));
+
+                var decoratorNode = (VisualNode)result.Root.Children[0];
+                Assert.Equal(0, decoratorNode.Children.Count);
+
+                Assert.Null(result.FindNode(border));
+                Assert.Null(result.FindNode(canvas));
+            }
+        }
+
+        [Fact]
+        public void Should_Update_Descendent_Tranform_When_Margin_Changed()
+        {
+            using (TestApplication())
+            {
+                Decorator decorator;
+                Border border;
+                Canvas canvas;
+                var tree = new TestRoot
+                {
+                    Width = 100,
+                    Height = 100,
+                    Child = decorator = new Decorator
+                    {
+                        Margin = new Thickness(0, 10, 0, 0),
+                        Child = border = new Border
+                        {
+                            Child = canvas = new Canvas(),
+                        }
+                    }
+                };
+
+                var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
+                layout.ExecuteInitialLayoutPass(tree);
+
+                var scene = new Scene(tree);
+                SceneBuilder.UpdateAll(scene);
+
+                var borderNode = scene.FindNode(border);
+                var canvasNode = scene.FindNode(canvas);
+                Assert.Equal(Matrix.CreateTranslation(0, 10), borderNode.Transform);
+                Assert.Equal(Matrix.CreateTranslation(0, 10), canvasNode.Transform);
+
+                decorator.Margin = new Thickness(0, 20, 0, 0);
+                layout.ExecuteLayoutPass();
+
+                scene = scene.Clone();
+                SceneBuilder.Update(scene, decorator);
+
+                borderNode = scene.FindNode(border);
+                canvasNode = scene.FindNode(canvas);
+                Assert.Equal(Matrix.CreateTranslation(0, 20), borderNode.Transform);
+                Assert.Equal(Matrix.CreateTranslation(0, 20), canvasNode.Transform);
             }
         }
+
+        private IDisposable TestApplication()
+        {
+            return UnitTestApplication.Start(
+                TestServices.MockPlatformRenderInterface.With(
+                    layoutManager: new LayoutManager()));
+        }
     }
 }

+ 86 - 4
tests/Avalonia.Visuals.UnitTests/VisualTests.cs

@@ -5,8 +5,10 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls;
+using Avalonia.Rendering;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Visuals.UnitTests
@@ -73,8 +75,19 @@ namespace Avalonia.Visuals.UnitTests
             var called1 = false;
             var called2 = false;
 
-            child1.AttachedToVisualTree += (s, e) => called1 = true;
-            child2.AttachedToVisualTree += (s, e) => called2 = true;
+            child1.AttachedToVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called1 = true;
+            };
+
+            child2.AttachedToVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called2 = true;
+            };
 
             root.Child = child1;
 
@@ -92,14 +105,83 @@ namespace Avalonia.Visuals.UnitTests
             var called2 = false;
 
             root.Child = child1;
-            child1.DetachedFromVisualTree += (s, e) => called1 = true;
-            child2.DetachedFromVisualTree += (s, e) => called2 = true;
+
+            child1.DetachedFromVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called1 = true;
+            };
+
+            child2.DetachedFromVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called2 = true;
+            };
+
             root.Child = null;
 
             Assert.True(called1);
             Assert.True(called2);
         }
 
+        [Fact]
+        public void Root_Should_Retun_Self_As_VisualRoot()
+        {
+            var root = new TestRoot();
+
+            Assert.Same(root, ((IVisual)root).VisualRoot);
+        }
+
+        [Fact]
+        public void Descendents_Should_RetunVisualRoot()
+        {
+            var root = new TestRoot();
+            var child1 = new Decorator();
+            var child2 = new Decorator();
+
+            root.Child = child1;
+            child1.Child = child2;
+
+            Assert.Same(root, ((IVisual)child1).VisualRoot);
+            Assert.Same(root, ((IVisual)child2).VisualRoot);
+        }
+
+        [Fact]
+        public void Attaching_To_Visual_Tree_Should_Invalidate_Visual()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (UnitTestApplication.Start(new TestServices(renderer: (root, loop) => renderer.Object)))
+            {
+                var child = new Decorator();
+                var root = new TestRoot();
+
+                root.Child = child;
+
+                renderer.Verify(x => x.AddDirty(child));
+            }                
+        }
+
+        [Fact]
+        public void Detaching_From_Visual_Tree_Should_Invalidate_Visual()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (UnitTestApplication.Start(new TestServices(renderer: (root, loop) => renderer.Object)))
+            {
+                var child = new Decorator();
+                var root = new TestRoot();
+
+                root.Child = child;
+                renderer.ResetCalls();
+                root.Child = null;
+
+                renderer.Verify(x => x.AddDirty(child));
+            }
+        }
+
         [Fact]
         public void Adding_Already_Parented_Control_Should_Throw()
         {

+ 5 - 5
tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

@@ -349,12 +349,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                         {
                             (target = new Border()
                             {
+                                Name = "b1",
                                 Width = 100,
                                 Height = 100,
                                 Background = Brushes.Red,
                             }),
                             new Border()
                             {
+                                Name = "b2",
                                 Width = 100,
                                 Height = 100,
                                 Background = Brushes.Red,
@@ -367,12 +369,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                                         {
                                             (item1 = new Border()
                                             {
+                                                Name = "b3",
                                                 Width = 100,
                                                 Height = 100,
                                                 Background = Brushes.Red,
                                             }),
                                             (item2 = new Border()
                                             {
+                                                Name = "b4",
                                                 Width = 100,
                                                 Height = 100,
                                                 Background = Brushes.Red,
@@ -400,19 +404,15 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
 
                 scroll.Offset = new Vector(0, 100);
 
-                //we don't have setup LayoutManager so we will make it manually
+                // We don't have LayoutManager set up so do the layout pass manually.
                 scroll.Parent.InvalidateArrange();
                 container.InvalidateArrange();
-
                 container.Arrange(new Rect(container.DesiredSize));
 
                 result = container.GetVisualsAt(new Point(50, 150)).First();
-
                 Assert.Equal(item2, result);
 
                 result = container.GetVisualsAt(new Point(50, 50)).First();
-
-                Assert.NotEqual(item1, result);
                 Assert.Equal(target, result);
             }
         }