Browse Source

Refactored VisualNode.

It now has two collections: Children and DrawOperations.
Steven Kirk 9 years ago
parent
commit
1f985abaa6

+ 1 - 2
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -116,9 +116,8 @@
     <Compile Include="Rendering\DefaultRenderLoop.cs" />
     <Compile Include="Rendering\SceneGraph\DeferredDrawingContextImpl.cs" />
     <Compile Include="Rendering\SceneGraph\GeometryNode.cs" />
-    <Compile Include="Rendering\SceneGraph\IGeometryNode.cs" />
+    <Compile Include="Rendering\SceneGraph\IDrawOperation.cs" />
     <Compile Include="Rendering\SceneGraph\ImageNode.cs" />
-    <Compile Include="Rendering\SceneGraph\ISceneNode.cs" />
     <Compile Include="Rendering\SceneGraph\IVisualNode.cs" />
     <Compile Include="Rendering\SceneGraph\LineNode.cs" />
     <Compile Include="Rendering\SceneGraph\RectangleNode.cs" />

+ 2 - 0
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -2,9 +2,11 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
 [assembly: AssemblyTitle("Avalonia.Visuals")]
+[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui/mutable", "Avalonia.Media.Mutable")]

+ 9 - 7
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -76,17 +76,19 @@ namespace Avalonia.Rendering
 
             if (!clipBounds.IsEmpty)
             {
-                node.Render(context);
+                node.BeginRender(context);
 
-                foreach (var child in node.Children)
+                foreach (var operation in node.DrawOperations)
                 {
-                    var visualChild = child as IVisualNode;
+                    operation.Render(context);
+                }
 
-                    if (visualChild != null)
-                    {
-                        Render(context, visualChild, clipBounds);
-                    }
+                foreach (var child in node.Children)
+                {
+                    Render(context, child, clipBounds);
                 }
+
+                node.EndRender(context);
             }
         }
 

+ 67 - 71
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -2,16 +2,16 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Collections.Generic;
-using System.Reactive.Disposables;
 using Avalonia.Media;
 using Avalonia.Platform;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public class DeferredDrawingContextImpl : IDrawingContextImpl
+    internal class DeferredDrawingContextImpl : IDrawingContextImpl
     {
-        private Stack<Frame> _stack = new Stack<Frame>();
+        private VisualNode _node;
+        private int _childIndex;
+        private int _drawOperationindex;
 
         public DeferredDrawingContextImpl()
             : this(new DirtyRects())
@@ -23,57 +23,47 @@ namespace Avalonia.Rendering.SceneGraph
             Dirty = dirty;
         }
 
-        public Matrix Transform { get; set; }
-
-        private VisualNode Node => _stack.Peek().Node;
+        public Matrix Transform { get; set; } = Matrix.Identity;
 
         public DirtyRects Dirty { get; }
 
-        private int Index
+        public UpdateState BeginUpdate(VisualNode node)
         {
-            get { return _stack.Peek().Index; }
-            set { _stack.Peek().Index = value; }
-        }
+            Contract.Requires<ArgumentNullException>(node != null);
 
-        public IDisposable Begin(VisualNode node)
-        {
-            if (_stack.Count > 0)
+            if (_node != null)
             {
-                var next = NextNodeAs<VisualNode>();
-
-                if (next == null || next != node)
+                if (_childIndex < _node.Children.Count)
                 {
-                    Add(node);
+                    _node.ReplaceChild(_childIndex, node);
                 }
                 else
                 {
-                    ++Index;
+                    _node.AddChild(node);
                 }
+
+                ++_childIndex;
             }
 
-            _stack.Push(new Frame(node));
-            return Disposable.Create(Pop);
+            var state = new UpdateState(this, _node, _childIndex, _drawOperationindex);
+            _node = node;
+            _childIndex = _drawOperationindex = 0;
+            return state;
         }
 
         public void Dispose()
         {
+            // Nothing to do here as we allocate no unmanaged resources.
         }
 
-        public void TrimNodes()
+        public void TrimChildren()
         {
-            var frame = _stack.Peek();
-            var children = frame.Node.Children;
-            var index = frame.Index;
-
-            if (children.Count > index)
-            {
-                children.RemoveRange(index, children.Count - index);
-            }
+            _node.TrimChildren(_childIndex);
         }
 
         public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
         {
-            var next = NextNodeAs<GeometryNode>();
+            var next = NextDrawAs<GeometryNode>();
 
             if (next == null || !next.Equals(Transform, brush, pen, geometry))
             {
@@ -81,13 +71,13 @@ namespace Avalonia.Rendering.SceneGraph
             }
             else
             {
-                ++Index;
+                ++_drawOperationindex;
             }
         }
 
         public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
         {
-            var next = NextNodeAs<ImageNode>();
+            var next = NextDrawAs<ImageNode>();
 
             if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect))
             {
@@ -95,13 +85,13 @@ namespace Avalonia.Rendering.SceneGraph
             }
             else
             {
-                ++Index;
+                ++_drawOperationindex;
             }
         }
 
         public void DrawLine(Pen pen, Point p1, Point p2)
         {
-            var next = NextNodeAs<LineNode>();
+            var next = NextDrawAs<LineNode>();
 
             if (next == null || !next.Equals(Transform, pen, p1, p2))
             {
@@ -109,13 +99,13 @@ namespace Avalonia.Rendering.SceneGraph
             }
             else
             {
-                ++Index;
+                ++_drawOperationindex;
             }
         }
 
         public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
         {
-            var next = NextNodeAs<RectangleNode>();
+            var next = NextDrawAs<RectangleNode>();
 
             if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius))
             {
@@ -123,13 +113,13 @@ namespace Avalonia.Rendering.SceneGraph
             }
             else
             {
-                ++Index;
+                ++_drawOperationindex;
             }
         }
 
         public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
         {
-            var next = NextNodeAs<TextNode>();
+            var next = NextDrawAs<TextNode>();
 
             if (next == null || !next.Equals(Transform, foreground, origin, text))
             {
@@ -137,13 +127,13 @@ namespace Avalonia.Rendering.SceneGraph
             }
             else
             {
-                ++Index;
+                ++_drawOperationindex;
             }
         }
 
         public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
         {
-            var next = NextNodeAs<RectangleNode>();
+            var next = NextDrawAs<RectangleNode>();
 
             if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius))
             {
@@ -151,7 +141,7 @@ namespace Avalonia.Rendering.SceneGraph
             }
             else
             {
-                ++Index;
+                ++_drawOperationindex;
             }
         }
 
@@ -195,51 +185,57 @@ namespace Avalonia.Rendering.SceneGraph
             // TODO: Implement
         }
 
-        private void Add(ISceneNode node)
+        public struct UpdateState : IDisposable
         {
-            var index = Index;
-
-            if (index < Node.Children.Count)
-            {
-                Node.Children[index] = node;
-            }
-            else
+            public UpdateState(
+                DeferredDrawingContextImpl owner,
+                VisualNode node,
+                int childIndex,
+                int drawOperationIndex)
             {
-                Node.Children.Add(node);
+                Owner = owner;
+                Node = node;
+                ChildIndex = childIndex;
+                DrawOperationIndex = drawOperationIndex;
             }
 
-            ++Index;
-        }
-
-        private T NextNodeAs<T>() where T : class, ISceneNode
-        {
-            return Index < Node.Children.Count ? Node.Children[Index] as T : null;
-        }
-
-        private void Pop()
-        {
-            foreach (var child in Node.Children)
+            public void Dispose()
             {
-                var geometry = child as IGeometryNode;
+                Owner._node.TrimDrawOperations(Owner._drawOperationindex);
 
-                if (geometry != null)
+                foreach (var operation in Owner._node.DrawOperations)
                 {
-                    Dirty.Add(geometry.Bounds);
+                    Owner.Dirty.Add(operation.Bounds);
                 }
+
+                Owner._node = Node;
+                Owner._childIndex = ChildIndex;
+                Owner._drawOperationindex = DrawOperationIndex;
             }
 
-            _stack.Pop();
+            public DeferredDrawingContextImpl Owner { get; }
+            public VisualNode Node { get; }
+            public int ChildIndex { get; }
+            public int DrawOperationIndex { get; }
         }
 
-        class Frame
+        private void Add(IDrawOperation  node)
         {
-            public Frame(VisualNode node)
+            if (_drawOperationindex < _node.DrawOperations.Count)
             {
-                Node = node;
+                _node.ReplaceDrawOperation(_drawOperationindex, node);
+            }
+            else
+            {
+                _node.AddDrawOperation(node);
             }
 
-            public VisualNode Node { get; }
-            public int Index { get; set; }
+            ++_drawOperationindex;
+        }
+
+        private T NextDrawAs<T>() where T : class, IDrawOperation
+        {
+            return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as T : null;
         }
     }
 }

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -7,7 +7,7 @@ using Avalonia.Platform;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public class GeometryNode : IGeometryNode
+    public class GeometryNode : IDrawOperation
     {
         public GeometryNode(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
         {

+ 13 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/IGeometryNode.cs → src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs

@@ -2,14 +2,20 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Media;
 
 namespace Avalonia.Rendering.SceneGraph
 {
     /// <summary>
     /// Represents a node in the low-level scene graph that represents geometry.
     /// </summary>
-    public interface IGeometryNode : ISceneNode
+    public interface IDrawOperation
     {
+        /// <summary>
+        /// Gets the bounds of the visible content in the node.
+        /// </summary>
+        Rect Bounds { get; }
+
         /// <summary>
         /// Hit test the geometry in this node.
         /// </summary>
@@ -20,5 +26,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// to hit test children they must be hit tested manually.
         /// </remarks>
         bool HitTest(Point p);
+
+        /// <summary>
+        /// Renders the node to a drawing context.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        void Render(IDrawingContextImpl context);
     }
 }

+ 0 - 25
src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs

@@ -1,25 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Media;
-
-namespace Avalonia.Rendering.SceneGraph
-{
-    /// <summary>
-    /// Represents a node in the low-level scene graph.
-    /// </summary>
-    public interface ISceneNode
-    {
-        /// <summary>
-        /// Gets the bounds of the visible content in the node.
-        /// </summary>
-        Rect Bounds { get; }
-
-        /// <summary>
-        /// Renders the node to a drawing context.
-        /// </summary>
-        /// <param name="context">The drawing context.</param>
-        void Render(IDrawingContextImpl context);
-    }
-}

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

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using Avalonia.Media;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Rendering.SceneGraph
@@ -10,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// Represents a node in the low-level scene graph representing an <see cref="IVisual"/>.
     /// </summary>
-    public interface IVisualNode : ISceneNode
+    public interface IVisualNode
     {
         /// <summary>
         /// Gets the visual to which the node relates.
@@ -44,7 +45,24 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// Gets the child scene graph nodes.
         /// </summary>
-        IReadOnlyList<ISceneNode> Children { get; }
+        IReadOnlyList<IVisualNode> Children { get; }
+
+        /// <summary>
+        /// Gets the drawing operations for the visual.
+        /// </summary>
+        IReadOnlyList<IDrawOperation> DrawOperations { get; }
+
+        /// <summary>
+        /// Sets up the drawing context for rendering the node's geometry.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        void BeginRender(IDrawingContextImpl context);
+
+        /// <summary>
+        /// Resets the drawing context after rendering the node's geometry.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        void EndRender(IDrawingContextImpl context);
 
         /// <summary>
         /// Hit test the geometry in this node.

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@@ -7,7 +7,7 @@ using Avalonia.Platform;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public class ImageNode : IGeometryNode
+    public class ImageNode : IDrawOperation
     {
         public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
         {

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@@ -6,7 +6,7 @@ using Avalonia.Media;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public class LineNode : IGeometryNode
+    public class LineNode : IDrawOperation
     {
         public LineNode(Matrix transform, Pen pen, Point p1, Point p2)
         {

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -6,7 +6,7 @@ using Avalonia.Media;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public class RectangleNode : IGeometryNode
+    public class RectangleNode : IDrawOperation
     {
         public RectangleNode(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius)
         {

+ 3 - 17
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -69,16 +69,7 @@ namespace Avalonia.Rendering.SceneGraph
 
             foreach (var child in source.Children)
             {
-                var visualNode = child as VisualNode;
-
-                if (visualNode != null)
-                {
-                    result.Children.Add(Clone(visualNode, result, index));
-                }
-                else
-                {
-                    result.Children.Add(child);
-                }
+                result.AddChild(Clone((VisualNode)child, result, index));
             }
 
             return result;
@@ -98,14 +89,9 @@ namespace Avalonia.Rendering.SceneGraph
                 {
                     for (var i = node.Children.Count - 1; i >= 0; --i)
                     {
-                        var visualChild = node.Children[i] as IVisualNode;
-
-                        if (visualChild != null)
+                        foreach (var h in HitTest(node.Children[i], p, clip, filter))
                         {
-                            foreach (var h in HitTest(visualChild, p, clip, filter))
-                            {
-                                yield return h;
-                            }
+                            yield return h;
                         }
                     }
 

+ 25 - 8
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -78,7 +78,7 @@ namespace Avalonia.Rendering.SceneGraph
                     {
                         // The control has been removed so remove it from its parent and deindex the
                         // node and its descendents.
-                        ((VisualNode)node.Parent)?.Children.Remove(node);
+                        ((VisualNode)node.Parent)?.RemoveChild(node);
                         Deindex(scene, node, dirty);
                         return true;
                     }
@@ -89,7 +89,7 @@ namespace Avalonia.Rendering.SceneGraph
                 // 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);
+                ((VisualNode)trim.Parent).RemoveChild(trim);
                 Deindex(scene, trim, dirty);
                 return true;
             }
@@ -148,12 +148,11 @@ namespace Avalonia.Rendering.SceneGraph
 
                 m = renderTransform * m;
 
-                using (contextImpl.Begin(node))
+                using (contextImpl.BeginUpdate(node))
                 using (context.PushPostTransform(m))
                 using (context.PushTransformContainer())
                 {
-                    forceRecurse = forceRecurse ||
-                        node.Transform != contextImpl.Transform;
+                    forceRecurse = forceRecurse || node.Transform != contextImpl.Transform;
 
                     node.Transform = contextImpl.Transform;
                     node.ClipBounds = (bounds * node.Transform).Intersect(clip);
@@ -167,7 +166,11 @@ namespace Avalonia.Rendering.SceneGraph
                         clip = clip.Intersect(node.ClipBounds);
                     }
 
-                    visual.Render(context);
+                    try
+                    {
+                        visual.Render(context);
+                    }
+                    catch { }
 
                     if (forceRecurse)
                     {
@@ -178,7 +181,11 @@ namespace Avalonia.Rendering.SceneGraph
                         }
 
                         node.SubTreeUpdated = true;
-                        contextImpl.TrimNodes();
+                        contextImpl.TrimChildren();
+                    }
+                    else if (node.OpacityChanged)
+                    {
+                        AddSubtreeBounds(node, contextImpl.Dirty);
                     }
                 }
             }
@@ -198,7 +205,7 @@ namespace Avalonia.Rendering.SceneGraph
 
             foreach (var child in node.Children)
             {
-                var geometry = child as IGeometryNode;
+                var geometry = child as IDrawOperation;
                 var visual = child as VisualNode;
 
                 if (geometry != null)
@@ -212,5 +219,15 @@ namespace Avalonia.Rendering.SceneGraph
                 }
             }
         }
+
+        private static void AddSubtreeBounds(VisualNode node, DirtyRects dirty)
+        {
+            dirty.Add(node.Bounds);
+
+            foreach (var child in node.Children)
+            {
+                AddSubtreeBounds((VisualNode)child, dirty);
+            }
+        }
     }
 }

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@@ -7,7 +7,7 @@ using Avalonia.Platform;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public class TextNode : IGeometryNode
+    public class TextNode : IDrawOperation
     {
         public TextNode(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
         {

+ 144 - 29
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -11,9 +11,16 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// A node in the low-level scene graph representing an <see cref="IVisual"/>.
     /// </summary>
-    public class VisualNode : IVisualNode
+    internal class VisualNode : IVisualNode
     {
+        private static readonly IReadOnlyList<IVisualNode> EmptyChildren = new IVisualNode[0];
+        private static readonly IReadOnlyList<IDrawOperation> EmptyDrawOperations = new IDrawOperation[0];
+
         private Rect? _bounds;
+        private double _opacity;
+        private List<IVisualNode> _children;
+        private List<IDrawOperation> _drawOperations;
+        private bool _drawOperationsCloned;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="VisualNode"/> class.
@@ -32,7 +39,6 @@ namespace Avalonia.Rendering.SceneGraph
 
             Visual = visual;
             Parent = parent;
-            Children = new List<ISceneNode>();
         }
 
         /// <inheritdoc/>
@@ -57,28 +63,122 @@ namespace Avalonia.Rendering.SceneGraph
         public Geometry GeometryClip { get; set; }
 
         /// <summary>
-        /// Gets or sets the opacity of the scnee graph node.
+        /// Gets or sets the opacity of the scene graph node.
         /// </summary>
-        public double Opacity { get; set; }
+        public double Opacity
+        {
+            get { return _opacity; }
+            set
+            {
+                if (_opacity != value)
+                {
+                    _opacity = value;
+                    OpacityChanged = true;
+                }
+            }
+        }
 
         /// <summary>
         /// Gets or sets the opacity mask for the scnee graph node.
         /// </summary>
         public IBrush OpacityMask { get; set; }
 
-        /// <summary>
-        /// Gets the child scene graph nodes.
-        /// </summary>
-        public List<ISceneNode> Children { get; }
-
         /// <summary>
         /// Gets a value indicating whether this node in the scene graph has already
         /// been updated in the current update pass.
         /// </summary>
         public bool SubTreeUpdated { get; set; }
 
+        /// <summary>
+        /// Gets a value indicating whether the <see cref="Opacity"/> property has changed.
+        /// </summary>
+        public bool OpacityChanged { get; private set; }
+
+        /// <inheritdoc/>
+        public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren;
+
         /// <inheritdoc/>
-        IReadOnlyList<ISceneNode> IVisualNode.Children => Children;
+        public IReadOnlyList<IDrawOperation> DrawOperations => _drawOperations ?? EmptyDrawOperations;
+
+        /// <summary>
+        /// Adds a child to the <see cref="Children"/> collection.
+        /// </summary>
+        /// <param name="child">The child to add.</param>
+        public void AddChild(IVisualNode child)
+        {
+            EnsureChildrenCreated();
+            _children.Add(child);
+        }
+
+        /// <summary>
+        /// Adds an operation to the <see cref="DrawOperations"/> collection.
+        /// </summary>
+        /// <param name="operation">The operation to add.</param>
+        public void AddDrawOperation(IDrawOperation operation)
+        {
+            EnsureDrawOperationsCreated();
+            _drawOperations.Add(operation);
+        }
+
+        /// <summary>
+        /// Removes a child from the <see cref="Children"/> collection.
+        /// </summary>
+        /// <param name="child">The child to remove.</param>
+        public void RemoveChild(IVisualNode child)
+        {
+            EnsureChildrenCreated();
+            _children.Remove(child);
+        }
+
+        /// <summary>
+        /// Replaces a child in the <see cref="Children"/> collection.
+        /// </summary>
+        /// <param name="index">The child to be replaced.</param>
+        /// <param name="node">The child to add.</param>
+        public void ReplaceChild(int index, IVisualNode node)
+        {
+            EnsureChildrenCreated();
+            _children[index] = node;
+        }
+
+        /// <summary>
+        /// Replaces an item in the <see cref="DrawOperations"/> collection.
+        /// </summary>
+        /// <param name="index">The opeation to be replaced.</param>
+        /// <param name="operation">The operation to add.</param>
+        public void ReplaceDrawOperation(int index, IDrawOperation operation)
+        {
+            EnsureDrawOperationsCreated();
+            _drawOperations[index] = operation;
+        }
+
+        /// <summary>
+        /// Removes items in the <see cref="Children"/> collection from the specified index
+        /// to the end.
+        /// </summary>
+        /// <param name="first">The index of the first child to be removed.</param>
+        public void TrimChildren(int first)
+        {
+            if (first < _children?.Count)
+            {
+                EnsureChildrenCreated();
+                _children.RemoveRange(first, _children.Count - first);
+            }
+        }
+
+        /// <summary>
+        /// Removes items in the <see cref="DrawOperations"/> collection from the specified index
+        /// to the end.
+        /// </summary>
+        /// <param name="first">The index of the first operation to be removed.</param>
+        public void TrimDrawOperations(int first)
+        {
+            if (first < _drawOperations?.Count)
+            {
+                EnsureDrawOperationsCreated();
+                _drawOperations.RemoveRange(first, _drawOperations.Count - first);
+            }
+        }
 
         /// <summary>
         /// Makes a copy of the node
@@ -93,19 +193,19 @@ namespace Avalonia.Rendering.SceneGraph
                 ClipBounds = ClipBounds,
                 ClipToBounds = ClipToBounds,
                 GeometryClip = GeometryClip,
-                Opacity = Opacity,
+                _opacity = Opacity,
                 OpacityMask = OpacityMask,
+                _drawOperations = _drawOperations,
+                _drawOperationsCloned = true,
             };
         }
 
         /// <inheritdoc/>
         public bool HitTest(Point p)
         {
-            foreach (var child in Children)
+            foreach (var operation in DrawOperations)
             {
-                var geometry = child as IGeometryNode;
-
-                if (geometry?.HitTest(p) == true)
+                if (operation.HitTest(p) == true)
                 {
                     return true;
                 }
@@ -114,7 +214,8 @@ namespace Avalonia.Rendering.SceneGraph
             return false;
         }
 
-        public void Render(IDrawingContextImpl context)
+        /// <inheritdoc/>
+        public void BeginRender(IDrawingContextImpl context)
         {
             context.Transform = Transform;
 
@@ -127,15 +228,11 @@ namespace Avalonia.Rendering.SceneGraph
             {
                 context.PushClip(ClipBounds * Transform.Invert());
             }
+        }
 
-            foreach (var child in Children)
-            {
-                if (!(child is IVisualNode))
-                {
-                    child.Render(context);
-                }
-            }
-
+        /// <inheritdoc/>
+        public void EndRender(IDrawingContextImpl context)
+        {
             if (ClipToBounds)
             {
                 context.PopClip();
@@ -151,16 +248,34 @@ namespace Avalonia.Rendering.SceneGraph
         {
             var result = new Rect();
 
-            foreach (var child in Children)
+            foreach (var operation in DrawOperations)
             {
-                if (!(child is IVisualNode))
-                {
-                    result = result.Union(child.Bounds);
-                }
+                result = result.Union(operation.Bounds);
             }
 
             _bounds = result;
             return result;
         }
+
+        private void EnsureChildrenCreated()
+        {
+            if (_children == null)
+            {
+                _children = new List<IVisualNode>();
+            }
+        }
+
+        private void EnsureDrawOperationsCreated()
+        {
+            if (_drawOperations == null)
+            {
+                _drawOperations = new List<IDrawOperation>();
+            }
+            else if (_drawOperationsCloned)
+            {
+                _drawOperations = new List<IDrawOperation>(_drawOperations);
+                _drawOperationsCloned = false;
+            }
+        }
     }
 }

+ 2 - 0
tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj

@@ -81,7 +81,9 @@
     <Compile Include="Media\PathMarkupParserTests.cs" />
     <Compile Include="RelativeRectComparer.cs" />
     <Compile Include="RelativeRectTests.cs" />
+    <Compile Include="Rendering\SceneGraph\DeferredDrawingContextImplTests.cs" />
     <Compile Include="Rendering\SceneGraph\SceneBuilderTests.cs" />
+    <Compile Include="Rendering\SceneGraph\VisualNodeTests.cs" />
     <Compile Include="ThicknessTests.cs" />
     <Compile Include="Media\BrushTests.cs" />
     <Compile Include="Media\ColorTests.cs" />

+ 179 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs

@@ -0,0 +1,179 @@
+using System;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Rendering;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
+{
+    public class DeferredDrawingContextImplTests
+    {
+        [Fact]
+        public void Should_Add_VisualNode()
+        {
+            var parent = new VisualNode(Mock.Of<IVisual>(), null);
+            var child = new VisualNode(Mock.Of<IVisual>(), null);
+            var target = new DeferredDrawingContextImpl();
+
+            target.BeginUpdate(parent);
+            target.BeginUpdate(child);
+
+            Assert.Equal(1, parent.Children.Count);
+            Assert.Same(child, parent.Children[0]);
+        }
+
+        [Fact]
+        public void Should_Not_Replace_Identical_VisualNode()
+        {
+            var parent = new VisualNode(Mock.Of<IVisual>(), null);
+            var child = new VisualNode(Mock.Of<IVisual>(), null);
+
+            parent.AddChild(child);
+
+            var target = new DeferredDrawingContextImpl();
+
+            target.BeginUpdate(parent);
+            target.BeginUpdate(child);
+
+            Assert.Equal(1, parent.Children.Count);
+            Assert.Same(child, parent.Children[0]);
+        }
+
+        [Fact]
+        public void Should_Replace_Different_VisualNode()
+        {
+            var parent = new VisualNode(Mock.Of<IVisual>(), null);
+            var child1 = new VisualNode(Mock.Of<IVisual>(), null);
+            var child2 = new VisualNode(Mock.Of<IVisual>(), null);
+
+            parent.AddChild(child1);
+
+            var target = new DeferredDrawingContextImpl();
+
+            target.BeginUpdate(parent);
+            target.BeginUpdate(child2);
+
+            Assert.Equal(1, parent.Children.Count);
+            Assert.Same(child2, parent.Children[0]);
+        }
+
+        [Fact]
+        public void TrimChildren_Should_Trim_Children()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+
+            node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
+            node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
+            node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
+            node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
+
+            var target = new DeferredDrawingContextImpl();
+            var child1 = new VisualNode(Mock.Of<IVisual>(), null);
+            var child2 = new VisualNode(Mock.Of<IVisual>(), null);
+
+            target.BeginUpdate(node);
+            using (target.BeginUpdate(child1)) { }
+            using (target.BeginUpdate(child2)) { }
+            target.TrimChildren();
+
+            Assert.Equal(2, node.Children.Count);
+        }
+
+        [Fact]
+        public void Should_Add_DrawOperations()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+            var target = new DeferredDrawingContextImpl();
+
+            using (target.BeginUpdate(node))
+            {
+                target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
+                target.DrawRectangle(new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100));
+            }
+
+            Assert.Equal(2, node.DrawOperations.Count);
+            Assert.IsType<RectangleNode>(node.DrawOperations[0]);
+            Assert.IsType<RectangleNode>(node.DrawOperations[1]);
+        }
+
+        [Fact]
+        public void Should_Not_Replace_Identical_DrawOperation()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+            var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
+            var target = new DeferredDrawingContextImpl();
+
+            node.AddDrawOperation(operation);
+
+            using (target.BeginUpdate(node))
+            {
+                target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
+            }
+
+            Assert.Equal(1, node.DrawOperations.Count);
+            Assert.Same(operation, node.DrawOperations.Single());
+
+            Assert.IsType<RectangleNode>(node.DrawOperations[0]);
+        }
+
+        [Fact]
+        public void Should_Replace_Different_DrawOperation()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+            var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
+            var target = new DeferredDrawingContextImpl();
+
+            node.AddDrawOperation(operation);
+
+            using (target.BeginUpdate(node))
+            {
+                target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
+            }
+
+            Assert.Equal(1, node.DrawOperations.Count);
+            Assert.NotSame(operation, node.DrawOperations.Single());
+
+            Assert.IsType<RectangleNode>(node.DrawOperations[0]);
+        }
+
+        [Fact]
+        public void Should_Update_DirtyRects()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+            var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
+            var dirtyRects = new DirtyRects();
+            var target = new DeferredDrawingContextImpl(dirtyRects);
+
+            using (target.BeginUpdate(node))
+            {
+                target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
+            }
+
+            Assert.Equal(new Rect(0, 0, 100, 100), dirtyRects.Single());
+        }
+
+        [Fact]
+        public void Should_Trim_DrawOperations()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+
+            node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 10, 100), 0));
+            node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 20, 100), 0));
+            node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 30, 100), 0));
+            node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 40, 100), 0));
+
+            var target = new DeferredDrawingContextImpl();
+
+            using (target.BeginUpdate(node))
+            {
+                target.FillRectangle(Brushes.Green, new Rect(0, 0, 10, 100));
+                target.FillRectangle(Brushes.Blue, new Rect(0, 0, 20, 100));
+            }
+
+            Assert.Equal(2, node.DrawOperations.Count);
+        }
+    }
+}

+ 15 - 12
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -45,17 +45,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Same(borderNode, result.FindNode(border));
                 Assert.Same(border, borderNode.Visual);
-                Assert.Equal(2, borderNode.Children.Count);
+                Assert.Equal(1, borderNode.Children.Count);
+                Assert.Equal(1, borderNode.DrawOperations.Count);
 
-                var backgroundNode = (RectangleNode)borderNode.Children[0];
+                var backgroundNode = (RectangleNode)borderNode.DrawOperations[0];
                 Assert.Equal(Brushes.Red, backgroundNode.Brush);
 
-                var textBlockNode = (VisualNode)borderNode.Children[1];
+                var textBlockNode = (VisualNode)borderNode.Children[0];
                 Assert.Same(textBlockNode, result.FindNode(textBlock));
                 Assert.Same(textBlock, textBlockNode.Visual);
-                Assert.Equal(1, textBlockNode.Children.Count);
+                Assert.Equal(1, textBlockNode.DrawOperations.Count);
 
-                var textNode = (TextNode)textBlockNode.Children[0];
+                var textNode = (TextNode)textBlockNode.DrawOperations[0];
                 Assert.NotNull(textNode.Text);
             }
         }
@@ -243,7 +244,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 SceneBuilder.UpdateAll(initial);
 
                 var initialBackgroundNode = initial.FindNode(border).Children[0];
-                var initialTextNode = initial.FindNode(textBlock).Children[0];
+                var initialTextNode = initial.FindNode(textBlock).DrawOperations[0];
 
                 Assert.NotNull(initialBackgroundNode);
                 Assert.NotNull(initialTextNode);
@@ -256,14 +257,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Same(border, borderNode.Visual);
 
-                var backgroundNode = (RectangleNode)borderNode.Children[0];
+                var backgroundNode = (RectangleNode)borderNode.DrawOperations[0];
                 Assert.NotSame(initialBackgroundNode, backgroundNode);
                 Assert.Equal(Brushes.Green, backgroundNode.Brush);
 
-                var textBlockNode = (VisualNode)borderNode.Children[1];
+                var textBlockNode = (VisualNode)borderNode.Children[0];
                 Assert.Same(textBlock, textBlockNode.Visual);
 
-                var textNode = (TextNode)textBlockNode.Children[0];
+                var textNode = (TextNode)textBlockNode.DrawOperations[0];
                 Assert.Same(initialTextNode, textNode);
             }
         }
@@ -306,9 +307,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 Assert.False(SceneBuilder.Update(result, canvas));
 
                 var borderNode = (VisualNode)result.Root.Children[0];
-                Assert.Equal(2, borderNode.Children.Count);
+                Assert.Equal(1, borderNode.Children.Count);
+                Assert.Equal(1, borderNode.DrawOperations.Count);
 
-                var decoratorNode = (VisualNode)borderNode.Children[1];
+                var decoratorNode = (VisualNode)borderNode.Children[0];
                 Assert.Same(decorator, decoratorNode.Visual);
                 Assert.Same(decoratorNode, result.FindNode(decorator));
 
@@ -356,7 +358,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 Assert.False(SceneBuilder.Update(result, canvas));
 
                 var borderNode = (VisualNode)result.Root.Children[0];
-                Assert.Equal(1, borderNode.Children.Count);
+                Assert.Equal(0, borderNode.Children.Count);
+                Assert.Equal(1, borderNode.DrawOperations.Count);
 
                 Assert.Null(result.FindNode(decorator));
             }

+ 80 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs

@@ -0,0 +1,80 @@
+using System;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
+{
+    public class VisualNodeTests
+    {
+        [Fact]
+        public void Empty_Children_Collections_Should_Be_Shared()
+        {
+            var node1 = new VisualNode(Mock.Of<IVisual>(), null);
+            var node2 = new VisualNode(Mock.Of<IVisual>(), null);
+
+            Assert.Same(node1.Children, node2.Children);
+        }
+
+        [Fact]
+        public void Adding_Child_Should_Create_Collection()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+            var collection = node.Children;
+
+            node.AddChild(Mock.Of<IVisualNode>());
+
+            Assert.NotSame(collection, node.Children);
+        }
+
+        [Fact]
+        public void Empty_DrawOperations_Collections_Should_Be_Shared()
+        {
+            var node1 = new VisualNode(Mock.Of<IVisual>(), null);
+            var node2 = new VisualNode(Mock.Of<IVisual>(), null);
+
+            Assert.Same(node1.DrawOperations, node2.DrawOperations);
+        }
+
+        [Fact]
+        public void Adding_DrawOperation_Should_Create_Collection()
+        {
+            var node = new VisualNode(Mock.Of<IVisual>(), null);
+            var collection = node.DrawOperations;
+
+            node.AddDrawOperation(Mock.Of<IDrawOperation>());
+
+            Assert.NotSame(collection, node.DrawOperations);
+        }
+
+        [Fact]
+        public void Cloned_Nodes_Should_Share_DrawOperations_Collection()
+        {
+            var node1 = new VisualNode(Mock.Of<IVisual>(), null);
+            node1.AddDrawOperation(Mock.Of<IDrawOperation>());
+
+            var node2 = node1.Clone(null);
+
+            Assert.Same(node1.DrawOperations, node2.DrawOperations);
+        }
+
+        [Fact]
+        public void Adding_DrawOperation_To_Cloned_Node_Should_Create_New_Collection()
+        {
+            var node1 = new VisualNode(Mock.Of<IVisual>(), null);
+            var operation1 = Mock.Of<IDrawOperation>();
+            node1.AddDrawOperation(operation1);
+
+            var node2 = node1.Clone(null);
+            var operation2 = Mock.Of<IDrawOperation>();
+            node2.ReplaceDrawOperation(0, operation2);
+
+            Assert.NotSame(node1.DrawOperations, node2.DrawOperations);
+            Assert.Equal(1, node1.DrawOperations.Count);
+            Assert.Equal(1, node2.DrawOperations.Count);
+            Assert.Same(operation1, node1.DrawOperations[0]);
+            Assert.Same(operation2, node2.DrawOperations[0]);
+        }
+    }
+}