Browse Source

Initial implementation of low-level scene graph.

Whole tree is currently being updated and rendered still though and lots doesn't even build.
Steven Kirk 9 years ago
parent
commit
ada15eba00
36 changed files with 1064 additions and 93 deletions
  1. 1 1
      samples/RenderTest/MainWindow.xaml.cs
  2. 1 1
      src/Avalonia.Controls/Shapes/Shape.cs
  3. 20 8
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  4. 19 22
      src/Avalonia.Visuals/Media/DrawingContext.cs
  5. 4 3
      src/Avalonia.Visuals/Media/IDrawingContextImpl.cs
  6. 1 1
      src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs
  7. 5 0
      src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
  8. 1 1
      src/Avalonia.Visuals/Platform/IRenderTarget.cs
  9. 128 0
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  10. 2 1
      src/Avalonia.Visuals/Rendering/IRenderer.cs
  11. 3 0
      src/Avalonia.Visuals/Rendering/Renderer.cs
  12. 28 28
      src/Avalonia.Visuals/Rendering/RendererMixin.cs
  13. 222 0
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  14. 39 0
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  15. 14 0
      src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs
  16. 15 0
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  17. 42 0
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  18. 35 0
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  19. 50 0
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  20. 72 0
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  21. 90 0
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  22. 39 0
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  23. 65 0
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  24. 3 3
      src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject
  25. 1 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  26. 3 3
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  27. 2 2
      src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
  28. 11 10
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  29. 4 0
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  30. 1 1
      src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs
  31. 1 1
      src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs
  32. 4 2
      src/Windows/Avalonia.Direct2D1/RenderTarget.cs
  33. 6 3
      tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject
  34. 1 0
      tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj
  35. 130 0
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  36. 1 1
      tests/Avalonia.Visuals.UnitTests/TestRoot.cs

+ 1 - 1
samples/RenderTest/MainWindow.xaml.cs

@@ -22,7 +22,7 @@ namespace RenderTest
             this.InitializeComponent();
             this.InitializeComponent();
             this.CreateAnimations();
             this.CreateAnimations();
             this.AttachDevTools();
             this.AttachDevTools();
-            RendererMixin.DrawFpsCounter = true;
+            Renderer.DrawFps = true;
         }
         }
 
 
         private void InitializeComponent()
         private void InitializeComponent()

+ 1 - 1
src/Avalonia.Controls/Shapes/Shape.cs

@@ -64,7 +64,7 @@ namespace Avalonia.Controls.Shapes
                     if (DefiningGeometry != null)
                     if (DefiningGeometry != null)
                     {
                     {
                         _renderedGeometry = DefiningGeometry.Clone();
                         _renderedGeometry = DefiningGeometry.Clone();
-                        _renderedGeometry.Transform = new MatrixTransform(_transform);
+                        ////_renderedGeometry.Transform = new MatrixTransform(_transform);
                     }
                     }
                 }
                 }
 
 

+ 20 - 8
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -100,15 +100,34 @@
     <Compile Include="Media\FormattedTextLine.cs" />
     <Compile Include="Media\FormattedTextLine.cs" />
     <Compile Include="Media\FormattedText.cs" />
     <Compile Include="Media\FormattedText.cs" />
     <Compile Include="Media\Geometry.cs" />
     <Compile Include="Media\Geometry.cs" />
-    <Compile Include="Media\IDrawingContext.cs" />
+    <Compile Include="Media\IDrawingContextImpl.cs" />
     <Compile Include="Platform\ExportRenderingSubsystemAttribute.cs" />
     <Compile Include="Platform\ExportRenderingSubsystemAttribute.cs" />
+    <Compile Include="Point.cs" />
+    <Compile Include="Rect.cs" />
+    <Compile Include="RelativePoint.cs" />
+    <Compile Include="RelativeRect.cs" />
+    <Compile Include="Rendering\DeferredRenderer.cs" />
     <Compile Include="Rendering\IRenderer.cs" />
     <Compile Include="Rendering\IRenderer.cs" />
     <Compile Include="Rendering\IRendererFactory.cs" />
     <Compile Include="Rendering\IRendererFactory.cs" />
     <Compile Include="Rendering\IRenderLoop.cs" />
     <Compile Include="Rendering\IRenderLoop.cs" />
     <Compile Include="Rendering\Renderer.cs" />
     <Compile Include="Rendering\Renderer.cs" />
     <Compile Include="Rendering\RendererMixin.cs" />
     <Compile Include="Rendering\RendererMixin.cs" />
     <Compile Include="Rendering\DefaultRenderLoop.cs" />
     <Compile Include="Rendering\DefaultRenderLoop.cs" />
+    <Compile Include="Rendering\SceneGraph\DeferredDrawingContextImpl.cs" />
+    <Compile Include="Rendering\SceneGraph\GeometryNode.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" />
+    <Compile Include="Rendering\SceneGraph\Scene.cs" />
+    <Compile Include="Rendering\SceneGraph\SceneBuilder.cs" />
+    <Compile Include="Rendering\SceneGraph\TextNode.cs" />
+    <Compile Include="Rendering\SceneGraph\VisualNode.cs" />
     <Compile Include="RenderTargetCorruptedException.cs" />
     <Compile Include="RenderTargetCorruptedException.cs" />
+    <Compile Include="Size.cs" />
+    <Compile Include="Thickness.cs" />
+    <Compile Include="Vector.cs" />
     <Compile Include="VisualTree\IVisual.cs" />
     <Compile Include="VisualTree\IVisual.cs" />
     <Compile Include="Media\Imaging\Bitmap.cs" />
     <Compile Include="Media\Imaging\Bitmap.cs" />
     <Compile Include="Media\Imaging\IBitmap.cs" />
     <Compile Include="Media\Imaging\IBitmap.cs" />
@@ -131,7 +150,6 @@
     <Compile Include="Media\ImageBush.cs" />
     <Compile Include="Media\ImageBush.cs" />
     <Compile Include="Media\VisualBrush.cs" />
     <Compile Include="Media\VisualBrush.cs" />
     <Compile Include="Platform\IPlatformSettings.cs" />
     <Compile Include="Platform\IPlatformSettings.cs" />
-    <Compile Include="RelativePoint.cs" />
     <Compile Include="Platform\IFormattedTextImpl.cs" />
     <Compile Include="Platform\IFormattedTextImpl.cs" />
     <Compile Include="Platform\IBitmapImpl.cs" />
     <Compile Include="Platform\IBitmapImpl.cs" />
     <Compile Include="Platform\IGeometryImpl.cs" />
     <Compile Include="Platform\IGeometryImpl.cs" />
@@ -140,14 +158,8 @@
     <Compile Include="Platform\IRenderTargetBitmapImpl.cs" />
     <Compile Include="Platform\IRenderTargetBitmapImpl.cs" />
     <Compile Include="Platform\IStreamGeometryContextImpl.cs" />
     <Compile Include="Platform\IStreamGeometryContextImpl.cs" />
     <Compile Include="Platform\IStreamGeometryImpl.cs" />
     <Compile Include="Platform\IStreamGeometryImpl.cs" />
-    <Compile Include="Point.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="RelativeRect.cs" />
-    <Compile Include="Rect.cs" />
     <Compile Include="Rendering\IRenderRoot.cs" />
     <Compile Include="Rendering\IRenderRoot.cs" />
-    <Compile Include="Size.cs" />
-    <Compile Include="Thickness.cs" />
-    <Compile Include="Vector.cs" />
     <Compile Include="VisualExtensions.cs" />
     <Compile Include="VisualExtensions.cs" />
     <Compile Include="Visual.cs" />
     <Compile Include="Visual.cs" />
     <Compile Include="VisualTreeAttachmentEventArgs.cs" />
     <Compile Include="VisualTreeAttachmentEventArgs.cs" />

+ 19 - 22
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -1,16 +1,11 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.Imaging;
 
 
 namespace Avalonia.Media
 namespace Avalonia.Media
 {
 {
     public sealed class DrawingContext : IDisposable
     public sealed class DrawingContext : IDisposable
     {
     {
-        private readonly IDrawingContextImpl _impl;
         private int _currentLevel;
         private int _currentLevel;
         //Internal tranformation that is applied but not exposed anywhere
         //Internal tranformation that is applied but not exposed anywhere
         //To be used for DPI scaling, etc
         //To be used for DPI scaling, etc
@@ -41,10 +36,11 @@ namespace Avalonia.Media
 
 
         public DrawingContext(IDrawingContextImpl impl, Matrix? hiddenPostTransform = null)
         public DrawingContext(IDrawingContextImpl impl, Matrix? hiddenPostTransform = null)
         {
         {
-            _impl = impl;
+            PlatformImpl = impl;
             _hiddenPostTransform = hiddenPostTransform;
             _hiddenPostTransform = hiddenPostTransform;
         }
         }
 
 
+        public IDrawingContextImpl PlatformImpl { get; }
 
 
         private Matrix _currentTransform = Matrix.Identity;
         private Matrix _currentTransform = Matrix.Identity;
 
 
@@ -62,7 +58,7 @@ namespace Avalonia.Media
                 var transform = _currentTransform*_currentContainerTransform;
                 var transform = _currentTransform*_currentContainerTransform;
                 if (_hiddenPostTransform.HasValue)
                 if (_hiddenPostTransform.HasValue)
                     transform = transform*_hiddenPostTransform.Value;
                     transform = transform*_hiddenPostTransform.Value;
-                _impl.Transform = transform;
+                PlatformImpl.Transform = transform;
             }
             }
         }
         }
 
 
@@ -79,7 +75,7 @@ namespace Avalonia.Media
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect)
         public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect)
-            => _impl.DrawImage(source, opacity, sourceRect, destRect);
+            => PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect);
 
 
         /// <summary>
         /// <summary>
         /// Draws a line.
         /// Draws a line.
@@ -87,7 +83,7 @@ namespace Avalonia.Media
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
-        public void DrawLine(Pen pen, Point p1, Point p2) => _impl.DrawLine(pen, p1, p2);
+        public void DrawLine(Pen pen, Point p1, Point p2) => PlatformImpl.DrawLine(pen, p1, p2);
 
 
         /// <summary>
         /// <summary>
         /// Draws a geometry.
         /// Draws a geometry.
@@ -95,7 +91,8 @@ namespace Avalonia.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="geometry">The geometry.</param>
-        public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) => _impl.DrawGeometry(brush, pen, geometry);
+        public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry)
+            => PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl);
 
 
         /// <summary>
         /// <summary>
         /// Draws the outline of a rectangle.
         /// Draws the outline of a rectangle.
@@ -104,7 +101,7 @@ namespace Avalonia.Media
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f)
         public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f)
-            => _impl.DrawRectangle(pen, rect, cornerRadius);
+            => PlatformImpl.DrawRectangle(pen, rect, cornerRadius);
 
 
         /// <summary>
         /// <summary>
         /// Draws text.
         /// Draws text.
@@ -113,7 +110,7 @@ namespace Avalonia.Media
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="text">The text.</param>
         /// <param name="text">The text.</param>
         public void DrawText(IBrush foreground, Point origin, FormattedText text)
         public void DrawText(IBrush foreground, Point origin, FormattedText text)
-            => _impl.DrawText(foreground, origin, text);
+            => PlatformImpl.DrawText(foreground, origin, text.PlatformImpl);
 
 
         /// <summary>
         /// <summary>
         /// Draws a filled rectangle.
         /// Draws a filled rectangle.
@@ -122,7 +119,7 @@ namespace Avalonia.Media
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f)
         public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f)
-            => _impl.FillRectangle(brush, rect, cornerRadius);
+            => PlatformImpl.FillRectangle(brush, rect, cornerRadius);
 
 
         public struct PushedState : IDisposable
         public struct PushedState : IDisposable
         {
         {
@@ -162,13 +159,13 @@ namespace Avalonia.Media
                 if (_type == PushedStateType.Matrix)
                 if (_type == PushedStateType.Matrix)
                     _context.CurrentTransform = _matrix;
                     _context.CurrentTransform = _matrix;
                 else if (_type == PushedStateType.Clip)
                 else if (_type == PushedStateType.Clip)
-                    _context._impl.PopClip();
+                    _context.PlatformImpl.PopClip();
                 else if (_type == PushedStateType.Opacity)
                 else if (_type == PushedStateType.Opacity)
-                    _context._impl.PopOpacity();
+                    _context.PlatformImpl.PopOpacity();
                 else if (_type == PushedStateType.GeometryClip)
                 else if (_type == PushedStateType.GeometryClip)
-                    _context._impl.PopGeometryClip();
+                    _context.PlatformImpl.PopGeometryClip();
                 else if (_type == PushedStateType.OpacityMask)
                 else if (_type == PushedStateType.OpacityMask)
-                    _context._impl.PopOpacityMask();
+                    _context.PlatformImpl.PopOpacityMask();
                 else if (_type == PushedStateType.MatrixContainer)
                 else if (_type == PushedStateType.MatrixContainer)
                 {
                 {
                     var cont = _context._transformContainers.Pop();
                     var cont = _context._transformContainers.Pop();
@@ -186,7 +183,7 @@ namespace Avalonia.Media
         /// <returns>A disposable used to undo the clip rectangle.</returns>
         /// <returns>A disposable used to undo the clip rectangle.</returns>
         public PushedState PushClip(Rect clip)
         public PushedState PushClip(Rect clip)
         {
         {
-            _impl.PushClip(clip);
+            PlatformImpl.PushClip(clip);
             return new PushedState(this, PushedState.PushedStateType.Clip);
             return new PushedState(this, PushedState.PushedStateType.Clip);
         }
         }
 
 
@@ -198,7 +195,7 @@ namespace Avalonia.Media
         public PushedState PushGeometryClip(Geometry clip)
         public PushedState PushGeometryClip(Geometry clip)
         {
         {
             Contract.Requires<ArgumentNullException>(clip != null);
             Contract.Requires<ArgumentNullException>(clip != null);
-            _impl.PushGeometryClip(clip);
+            PlatformImpl.PushGeometryClip(clip);
             return new PushedState(this, PushedState.PushedStateType.GeometryClip);
             return new PushedState(this, PushedState.PushedStateType.GeometryClip);
         }
         }
 
 
@@ -210,7 +207,7 @@ namespace Avalonia.Media
         public PushedState PushOpacity(double opacity)
         public PushedState PushOpacity(double opacity)
             //TODO: Eliminate platform-specific push opacity call
             //TODO: Eliminate platform-specific push opacity call
         {
         {
-            _impl.PushOpacity(opacity);
+            PlatformImpl.PushOpacity(opacity);
             return new PushedState(this, PushedState.PushedStateType.Opacity);
             return new PushedState(this, PushedState.PushedStateType.Opacity);
         }
         }
 
 
@@ -224,7 +221,7 @@ namespace Avalonia.Media
         /// <returns>A disposable to undo the opacity mask.</returns>
         /// <returns>A disposable to undo the opacity mask.</returns>
         public PushedState PushOpacityMask(IBrush mask, Rect bounds)
         public PushedState PushOpacityMask(IBrush mask, Rect bounds)
         {
         {
-            _impl.PushOpacityMask(mask, bounds);
+            PlatformImpl.PushOpacityMask(mask, bounds);
             return new PushedState(this, PushedState.PushedStateType.OpacityMask);
             return new PushedState(this, PushedState.PushedStateType.OpacityMask);
         }
         }
 
 
@@ -278,7 +275,7 @@ namespace Avalonia.Media
             _states = null;
             _states = null;
             TransformStackPool.Push(_transformContainers);
             TransformStackPool.Push(_transformContainers);
             _transformContainers = null;
             _transformContainers = null;
-            _impl.Dispose();
+            PlatformImpl.Dispose();
         }
         }
     }
     }
 }
 }

+ 4 - 3
src/Avalonia.Visuals/Media/IDrawingContext.cs → src/Avalonia.Visuals/Media/IDrawingContextImpl.cs

@@ -3,6 +3,7 @@
 
 
 using System;
 using System;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.Imaging;
+using Avalonia.Platform;
 
 
 namespace Avalonia.Media
 namespace Avalonia.Media
 {
 {
@@ -23,7 +24,7 @@ namespace Avalonia.Media
         /// <param name="opacity">The opacity to draw with.</param>
         /// <param name="opacity">The opacity to draw with.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
-        void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect);
+        void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect);
 
 
         /// <summary>
         /// <summary>
         /// Draws a line.
         /// Draws a line.
@@ -39,7 +40,7 @@ namespace Avalonia.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="geometry">The geometry.</param>
-        void DrawGeometry(IBrush brush, Pen pen, Geometry geometry);
+        void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry);
 
 
         /// <summary>
         /// <summary>
         /// Draws the outline of a rectangle.
         /// Draws the outline of a rectangle.
@@ -55,7 +56,7 @@ namespace Avalonia.Media
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="text">The text.</param>
         /// <param name="text">The text.</param>
-        void DrawText(IBrush foreground, Point origin, FormattedText text);
+        void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
 
 
         /// <summary>
         /// <summary>
         /// Draws a filled rectangle.
         /// Draws a filled rectangle.

+ 1 - 1
src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs

@@ -48,6 +48,6 @@ namespace Avalonia.Media.Imaging
             return factory.CreateRenderTargetBitmap(width, height);
             return factory.CreateRenderTargetBitmap(width, height);
         }
         }
 
 
-        public DrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext();
+        public IDrawingContextImpl CreateDrawingContext() => PlatformImpl.CreateDrawingContext();
     }
     }
 }
 }

+ 5 - 0
src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs

@@ -17,6 +17,11 @@ namespace Avalonia.Platform
         /// </summary>
         /// </summary>
         Size Constraint { get; set; }
         Size Constraint { get; set; }
 
 
+        /// <summary>
+        /// Gets the text.
+        /// </summary>
+        string Text { get; }
+
         /// <summary>
         /// <summary>
         /// Gets the lines in the text.
         /// Gets the lines in the text.
         /// </summary>
         /// </summary>

+ 1 - 1
src/Avalonia.Visuals/Platform/IRenderTarget.cs

@@ -17,6 +17,6 @@ namespace Avalonia.Platform
         /// <summary>
         /// <summary>
         /// Creates an <see cref="DrawingContext"/> for a rendering session.
         /// Creates an <see cref="DrawingContext"/> for a rendering session.
         /// </summary>
         /// </summary>
-        DrawingContext CreateDrawingContext();
+        IDrawingContextImpl CreateDrawingContext();
     }
     }
 }
 }

+ 128 - 0
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -0,0 +1,128 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class DeferredRenderer : IRenderer
+    {
+        private readonly IRenderLoop _renderLoop;
+        private readonly IRenderRoot _root;
+        private Scene _scene;
+        private IRenderTarget _renderTarget;
+        private bool _needsUpdate;
+        private bool _needsRender;
+
+        private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+        private int _totalFrames;
+        private int _framesThisSecond;
+        private int _fps;
+        private TimeSpan _lastFpsUpdate;
+
+        public DeferredRenderer(IRenderRoot root, IRenderLoop renderLoop)
+        {
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            _root = root;
+            _scene = new Scene(root);
+            _renderLoop = renderLoop;
+            _renderLoop.Tick += OnRenderLoopTick;
+        }
+
+        public bool DrawFps { get; set; }
+
+        public void AddDirty(IVisual visual)
+        {
+            if (!_needsUpdate)
+            {
+                _needsUpdate = true;
+                Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
+            }
+        }
+
+        public void Dispose()
+        {
+            _renderLoop.Tick -= OnRenderLoopTick;
+        }
+
+        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)
+        {
+            var now = _stopwatch.Elapsed;
+            var elapsed = now - _lastFpsUpdate;
+
+            _framesThisSecond++;
+
+            if (elapsed.TotalSeconds > 1)
+            {
+                _fps = (int)(_framesThisSecond / elapsed.TotalSeconds);
+                _framesThisSecond = 0;
+                _lastFpsUpdate = now;
+            }
+
+            var pt = new Point(40, 40);
+            using (
+                var txt = new FormattedText("Frame #" + _totalFrames + " FPS: " + _fps, "Arial", 18,
+                    FontStyle.Normal,
+                    TextAlignment.Left,
+                    FontWeight.Normal,
+                    TextWrapping.NoWrap))
+            {
+                context.Transform = Matrix.Identity;
+                context.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
+                context.DrawText(Brushes.Black, pt, txt.PlatformImpl);
+            }
+        }
+
+        private void UpdateScene()
+        {
+            Dispatcher.UIThread.VerifyAccess();
+
+            _scene = SceneBuilder.Update(_scene);
+            _needsUpdate = false;
+            _needsRender = true;
+            _root.Invalidate(new Rect(_root.ClientSize));
+        }
+
+        private void OnRenderLoopTick(object sender, EventArgs e)
+        {
+            //if (_needsRender)
+            //{
+            //    _needsRender = false;
+            //}
+        }
+    }
+}

+ 2 - 1
src/Avalonia.Visuals/Rendering/IRenderer.cs

@@ -8,8 +8,9 @@ namespace Avalonia.Rendering
 {
 {
     public interface IRenderer : IDisposable
     public interface IRenderer : IDisposable
     {
     {
-        void AddDirty(IVisual visual);
+        bool DrawFps { get; set; }
 
 
+        void AddDirty(IVisual visual);
         void Render(Rect rect);
         void Render(Rect rect);
     }
     }
 }
 }

+ 3 - 0
src/Avalonia.Visuals/Rendering/Renderer.cs

@@ -23,6 +23,8 @@ namespace Avalonia.Rendering
             _renderLoop.Tick += OnRenderLoopTick;
             _renderLoop.Tick += OnRenderLoopTick;
         }
         }
 
 
+        public bool DrawFps { get; set; }
+
         public void AddDirty(IVisual visual)
         public void AddDirty(IVisual visual)
         {
         {
             _dirty = true;
             _dirty = true;
@@ -42,6 +44,7 @@ namespace Avalonia.Rendering
 
 
             try
             try
             {
             {
+                RendererMixin.DrawFpsCounter = DrawFps;
                 _renderTarget.Render(_root);
                 _renderTarget.Render(_root);
             }
             }
             catch (RenderTargetCorruptedException ex)
             catch (RenderTargetCorruptedException ex)

+ 28 - 28
src/Avalonia.Visuals/Rendering/RendererMixin.cs

@@ -42,34 +42,34 @@ namespace Avalonia.Rendering
         /// <param name="visual">The visual to render.</param>
         /// <param name="visual">The visual to render.</param>
         public static void Render(this IRenderTarget renderTarget, IVisual visual)
         public static void Render(this IRenderTarget renderTarget, IVisual visual)
         {
         {
-            using (var ctx = renderTarget.CreateDrawingContext())
-            {
-                ctx.Render(visual);
-                s_frameNum++;
-                if (DrawFpsCounter)
-                {
-                    s_currentFrames++;
-                    var now = s_stopwatch.Elapsed;
-                    var elapsed = now - s_lastMeasure;
-                    if (elapsed.TotalSeconds > 1)
-                    {
-                        s_fps = (int) (s_currentFrames/elapsed.TotalSeconds);
-                        s_currentFrames = 0;
-                        s_lastMeasure = now;
-                    }
-                    var pt = new Point(40, 40);
-                    using (
-                        var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18,
-                            FontStyle.Normal,
-                            TextAlignment.Left,
-                            FontWeight.Normal,
-                            TextWrapping.NoWrap))
-                    {
-                        ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
-                        ctx.DrawText(Brushes.Black, pt, txt);
-                    }
-                }
-            }
+            ////using (var ctx = renderTarget.CreateDrawingContext())
+            ////{
+            ////    ctx.Render(visual);
+            ////    s_frameNum++;
+            ////    if (DrawFpsCounter)
+            ////    {
+            ////        s_currentFrames++;
+            ////        var now = s_stopwatch.Elapsed;
+            ////        var elapsed = now - s_lastMeasure;
+            ////        if (elapsed.TotalSeconds > 1)
+            ////        {
+            ////            s_fps = (int) (s_currentFrames/elapsed.TotalSeconds);
+            ////            s_currentFrames = 0;
+            ////            s_lastMeasure = now;
+            ////        }
+            ////        var pt = new Point(40, 40);
+            ////        using (
+            ////            var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18,
+            ////                FontStyle.Normal,
+            ////                TextAlignment.Left,
+            ////                FontWeight.Normal,
+            ////                TextWrapping.NoWrap))
+            ////        {
+            ////            ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
+            ////            ctx.DrawText(Brushes.Black, pt, txt);
+            ////        }
+            ////    }
+            ////}
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 222 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -0,0 +1,222 @@
+// 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 System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public class DeferredDrawingContextImpl : IDrawingContextImpl
+    {
+        private Stack<Frame> _stack = new Stack<Frame>();
+
+        public Matrix Transform { get; set; }
+
+        private VisualNode Node => _stack.Peek().Node;
+
+        private int Index
+        {
+            get { return _stack.Peek().Index; }
+            set { _stack.Peek().Index = value; }
+        }
+
+        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)
+                {
+                    Add(visualNode);
+                }
+                else
+                {
+                    ++Index;
+                }
+            }
+        }
+
+        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+        {
+            var next = NextNodeAs<GeometryNode>();
+
+            if (next == null || !next.Equals(Transform, brush, pen, geometry))
+            {
+                Add(new GeometryNode(Transform, brush, pen, geometry));
+            }
+            else
+            {
+                ++Index;
+            }
+        }
+
+        public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
+        {
+            var next = NextNodeAs<ImageNode>();
+
+            if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect))
+            {
+                Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
+            }
+            else
+            {
+                ++Index;
+            }
+        }
+
+        public void DrawLine(Pen pen, Point p1, Point p2)
+        {
+            var next = NextNodeAs<LineNode>();
+
+            if (next == null || !next.Equals(Transform, pen, p1, p2))
+            {
+                Add(new LineNode(Transform, pen, p1, p2));
+            }
+            else
+            {
+                ++Index;
+            }
+        }
+
+        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+        {
+            var next = NextNodeAs<RectangleNode>();
+
+            if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius))
+            {
+                Add(new RectangleNode(Transform, null, pen, rect, cornerRadius));
+            }
+            else
+            {
+                ++Index;
+            }
+        }
+
+        public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+        {
+            var next = NextNodeAs<TextNode>();
+
+            if (next == null || !next.Equals(Transform, foreground, origin, text))
+            {
+                Add(new TextNode(Transform, foreground, origin, text));
+            }
+            else
+            {
+                ++Index;
+            }
+        }
+
+        public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+        {
+            var next = NextNodeAs<RectangleNode>();
+
+            if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius))
+            {
+                Add(new RectangleNode(Transform, brush, null, rect, cornerRadius));
+            }
+            else
+            {
+                ++Index;
+            }
+        }
+
+        public void PopClip()
+        {
+            // TODO: Implement
+        }
+
+        public void PopGeometryClip()
+        {
+            // TODO: Implement
+        }
+
+        public void PopOpacity()
+        {
+            // TODO: Implement
+        }
+
+        public void PopOpacityMask()
+        {
+            // TODO: Implement
+        }
+
+        public void PushClip(Rect clip)
+        {
+            // TODO: Implement
+        }
+
+        public void PushGeometryClip(Geometry clip)
+        {
+            // TODO: Implement
+        }
+
+        public void PushOpacity(double opacity)
+        {
+            // TODO: Implement
+        }
+
+        public void PushOpacityMask(IBrush mask, Rect bounds)
+        {
+            // TODO: Implement
+        }
+
+        private void Add(ISceneNode node)
+        {
+            var index = Index;
+
+            if (index < Node.Children.Count)
+            {
+                Node.Children[index] = node;
+            }
+            else
+            {
+                Node.Children.Add(node);
+            }
+
+            ++Index;
+        }
+
+        private T NextNodeAs<T>() where T : class, ISceneNode
+        {
+            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);
+            }
+        }
+
+        class Frame
+        {
+            public Frame(VisualNode node)
+            {
+                Node = node;
+            }
+
+            public VisualNode Node { get; }
+            public int Index { get; set; }
+        }
+    }
+}

+ 39 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -0,0 +1,39 @@
+// 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;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public class GeometryNode : ISceneNode
+    {
+        public GeometryNode(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
+        {
+            Transform = transform;
+            Brush = brush;
+            Pen = pen;
+            Geometry = geometry;
+        }
+
+        public Matrix Transform { get; }
+        public IBrush Brush { get; }
+        public Pen Pen { get; }
+        public IGeometryImpl Geometry { get; }
+
+        public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
+        {
+            return transform == Transform &&
+                Equals(brush, Brush) && 
+                pen == Pen &&
+                Equals(geometry, Geometry);
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawGeometry(Brush, Pen, Geometry);
+        }
+    }
+}

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

@@ -0,0 +1,14 @@
+// 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 System.Collections.Generic;
+using Avalonia.Media;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public interface ISceneNode
+    {
+        void Render(IDrawingContextImpl context);
+    }
+}

+ 15 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@@ -0,0 +1,15 @@
+// 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 System.Collections.Generic;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public interface IVisualNode : ISceneNode
+    {
+        IReadOnlyList<ISceneNode> Children { get; }
+        IVisual Visual { get; }
+    }
+}

+ 42 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@@ -0,0 +1,42 @@
+// 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;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public class ImageNode : ISceneNode
+    {
+        public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
+        {
+            Transform = transform;
+            Source = source;
+            Opacity = opacity;
+            SourceRect = sourceRect;
+            DestRect = destRect;
+        }
+
+        public Matrix Transform { get; }
+        public IBitmapImpl Source { get; }
+        public double Opacity { get; }
+        public Rect SourceRect { get; }
+        public Rect DestRect { get; }
+
+        public bool Equals(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
+        {
+            return transform == Transform &&
+                Equals(source, Source) &&
+                opacity == Opacity &&
+                sourceRect == SourceRect &&
+                destRect == DestRect;
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawImage(Source, Opacity, SourceRect, DestRect);
+        }
+    }
+}

+ 35 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@@ -0,0 +1,35 @@
+// 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
+{
+    public class LineNode : ISceneNode
+    {
+        public LineNode(Matrix transform, Pen pen, Point p1, Point p2)
+        {
+            Transform = transform;
+            Pen = pen;
+            P1 = p1;
+            P2 = p2;
+        }
+
+        public Matrix Transform { get; }
+        public Pen Pen { get; }
+        public Point P1 { get; }
+        public Point P2 { get; }
+
+        public bool Equals(Matrix transform, Pen pen, Point p1, Point p2)
+        {
+            return transform == Transform && pen == Pen && p1 == P1 && p2 == P2;
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawLine(Pen, P1, P2);
+        }
+    }
+}

+ 50 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -0,0 +1,50 @@
+// 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
+{
+    public class RectangleNode : ISceneNode
+    {
+        public RectangleNode(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius)
+        {
+            Transform = transform;
+            Brush = brush;
+            Pen = pen;
+            Rect = rect;
+            CornerRadius = cornerRadius;
+        }
+
+        public Matrix Transform { get; }
+        public IBrush Brush { get; }
+        public Pen Pen { get; }
+        public Rect Rect { get; }
+        public float CornerRadius { get; }
+
+        public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius)
+        {
+            return transform == Transform &&
+                Equals(brush, Brush) &&
+                pen == Pen &&
+                rect == Rect &&
+                cornerRadius == CornerRadius;
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+
+            if (Brush != null)
+            {
+                context.FillRectangle(Brush, Rect, CornerRadius);
+            }
+
+            if (Pen != null)
+            {
+                context.DrawRectangle(Pen, Rect, CornerRadius);
+            }
+        }
+    }
+}

+ 72 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -0,0 +1,72 @@
+// 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 System.Collections.Generic;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public class Scene
+    {
+        private Dictionary<IVisual, IVisualNode> _index;
+
+        public Scene(IVisual rootVisual)
+            : this(new VisualNode(rootVisual), new Dictionary<IVisual, IVisualNode>())
+        {
+        }
+
+        internal Scene(VisualNode root, Dictionary<IVisual, IVisualNode> index)
+        {
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            _index = index;
+            Root = root;
+        }
+
+        public IVisualNode Root { get; }
+
+        public void Add(IVisualNode node)
+        {
+            _index.Add(node.Visual, node);
+        }
+
+        public IVisualNode FindNode(IVisual visual)
+        {
+            IVisualNode node;
+            _index.TryGetValue(visual, out node);
+            return node;
+        }
+
+        public Scene Clone()
+        {
+            var index = new Dictionary<IVisual, IVisualNode>();
+            var root = (VisualNode)Clone((VisualNode)Root, null, index);
+            var result = new Scene(root, index);
+            return result;
+        }
+
+        private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary<IVisual, IVisualNode> index)
+        {
+            var result = source.Clone();
+
+            index.Add(result.Visual, result);
+
+            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);
+                }
+            }
+
+            return result;
+        }
+    }
+}

+ 90 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -0,0 +1,90 @@
+// 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;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public static class SceneBuilder
+    {
+        public static Scene Update(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);
+            }
+
+            return scene;
+        }
+
+        private static void Update(DrawingContext context, Scene scene, IVisual visual, VisualNode parent)
+        {
+            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)
+            {
+                var m = Matrix.CreateTranslation(visual.Bounds.Position);
+
+                var renderTransform = Matrix.Identity;
+
+                if (visual.RenderTransform != null)
+                {
+                    var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+                    var offset = Matrix.CreateTranslation(origin);
+                    renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
+                }
+
+                m = renderTransform * m;
+
+                using (contextImpl.Begin(node))
+                using (context.PushPostTransform(m))
+                using (context.PushTransformContainer())
+                {
+                    node.Transform = contextImpl.Transform;
+                    node.Bounds = bounds;
+                    node.ClipToBounds = clipToBounds;
+                    node.GeometryClip = visual.Clip;
+                    node.Opacity = opacity;
+                    node.OpacityMask = visual.OpacityMask;
+
+                    visual.Render(context);
+
+#pragma warning disable 0618
+                    var transformed = new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
+#pragma warning restore 0618
+
+                    if (visual is Visual)
+                    {
+                        BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
+                    }
+
+                    foreach (var child in visual.VisualChildren)
+                    {
+                        Update(context, scene, child, node);
+                    }
+                }
+            }
+        }
+
+        private static VisualNode CreateNode(IVisual visual, Scene scene, VisualNode parent)
+        {
+            var node = new VisualNode(visual);
+            scene.Add(node);
+            return node;
+        }
+    }
+}

+ 39 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@@ -0,0 +1,39 @@
+// 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;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public class TextNode : ISceneNode
+    {
+        public TextNode(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
+        {
+            Transform = transform;
+            Foreground = foreground;
+            Origin = origin;
+            Text = text;
+        }
+
+        public Matrix Transform { get; }
+        public IBrush Foreground { get; }
+        public Point Origin { get; }
+        public IFormattedTextImpl Text { get; }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawText(Foreground, Origin, Text);
+        }
+
+        internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
+        {
+            return transform == Transform &&
+                Equals(foreground, Foreground) &&
+                origin == Origin &&
+                Equals(text, Text);
+        }
+    }
+}

+ 65 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -0,0 +1,65 @@
+// 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 System.Collections.Generic;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public class VisualNode : IVisualNode
+    {
+        public VisualNode(IVisual visual)
+        {
+            Children = new List<ISceneNode>();
+            Visual = visual;
+        }
+
+        public IVisual Visual { get; }
+        public Matrix Transform { get; set; }
+        public Rect Bounds { get; set; }
+        public bool ClipToBounds { get; set; }
+        public Geometry GeometryClip { get; set; }
+        public double Opacity { get; set; }
+        public IBrush OpacityMask { get; set; }
+        public List<ISceneNode> Children { get; }
+
+        IReadOnlyList<ISceneNode> IVisualNode.Children => Children;
+
+        public VisualNode Clone()
+        {
+            return new VisualNode(Visual);
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+
+            if (Opacity != 1)
+            {
+                context.PushOpacity(Opacity);
+            }
+
+            if (ClipToBounds)
+            {
+                context.PushClip(Bounds);
+            }
+
+            foreach (var child in Children)
+            {
+                child.Render(context);
+            }
+
+            if (ClipToBounds)
+            {
+                context.PopClip();
+            }
+
+            if (Opacity != 1)
+            {
+                context.PopOpacity();
+            }
+        }
+    }
+}

+ 3 - 3
src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject

@@ -17,9 +17,9 @@
   <DetectStackOverflow>true</DetectStackOverflow>
   <DetectStackOverflow>true</DetectStackOverflow>
   <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
   <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
   <DefaultTestTimeout>60000</DefaultTestTimeout>
   <DefaultTestTimeout>60000</DefaultTestTimeout>
-  <UseBuildConfiguration />
-  <UseBuildPlatform />
-  <ProxyProcessPath />
+  <UseBuildConfiguration></UseBuildConfiguration>
+  <UseBuildPlatform></UseBuildPlatform>
+  <ProxyProcessPath></ProxyProcessPath>
   <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
   <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
   <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
   <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
   <BuildProcessArchitecture>x86</BuildProcessArchitecture>
   <BuildProcessArchitecture>x86</BuildProcessArchitecture>

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@@ -64,7 +64,7 @@
     <Compile Include="Disposable.cs" />
     <Compile Include="Disposable.cs" />
     <Compile Include="Media\BrushImpl.cs" />
     <Compile Include="Media\BrushImpl.cs" />
     <Compile Include="Media\BrushWrapper.cs" />
     <Compile Include="Media\BrushWrapper.cs" />
-    <Compile Include="Media\DrawingContext.cs" />
+    <Compile Include="Media\DrawingContextImpl.cs" />
     <Compile Include="Media\Imaging\RenderTargetBitmapImpl.cs" />
     <Compile Include="Media\Imaging\RenderTargetBitmapImpl.cs" />
     <Compile Include="Media\Imaging\BitmapImpl.cs" />
     <Compile Include="Media\Imaging\BitmapImpl.cs" />
     <Compile Include="Media\RadialGradientBrushImpl.cs" />
     <Compile Include="Media\RadialGradientBrushImpl.cs" />

+ 3 - 3
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -29,9 +29,9 @@ namespace Avalonia.Direct2D1
 
 
         private static readonly SharpDX.Direct2D1.Factory s_d2D1Factory =
         private static readonly SharpDX.Direct2D1.Factory s_d2D1Factory =
 #if DEBUG
 #if DEBUG
-            new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.SingleThreaded, SharpDX.Direct2D1.DebugLevel.Error);
+            new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.MultiThreaded, SharpDX.Direct2D1.DebugLevel.Error);
 #else
 #else
-            new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.SingleThreaded, SharpDX.Direct2D1.DebugLevel.None);
+            new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.MultiThreaded, SharpDX.Direct2D1.DebugLevel.None);
 #endif
 #endif
         private static readonly SharpDX.DirectWrite.Factory s_dwfactory = new SharpDX.DirectWrite.Factory();
         private static readonly SharpDX.DirectWrite.Factory s_dwfactory = new SharpDX.DirectWrite.Factory();
 
 
@@ -63,7 +63,7 @@ namespace Avalonia.Direct2D1
 
 
         public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
         public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
         {
         {
-            return new Renderer(root, renderLoop);
+            return new DeferredRenderer(root, renderLoop);
         }
         }
 
 
         public IRenderTarget CreateRenderTarget(IPlatformHandle handle)
         public IRenderTarget CreateRenderTarget(IPlatformHandle handle)

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs

@@ -11,14 +11,14 @@ namespace Avalonia.Direct2D1.Media
 {
 {
     internal class AvaloniaTextRenderer : TextRenderer
     internal class AvaloniaTextRenderer : TextRenderer
     {
     {
-        private readonly DrawingContext _context;
+        private readonly DrawingContextImpl _context;
 
 
         private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
         private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
 
 
         private readonly Brush _foreground;
         private readonly Brush _foreground;
 
 
         public AvaloniaTextRenderer(
         public AvaloniaTextRenderer(
-            DrawingContext context,
+            DrawingContextImpl context,
             SharpDX.Direct2D1.RenderTarget target,
             SharpDX.Direct2D1.RenderTarget target,
             Brush foreground)
             Brush foreground)
         {
         {

+ 11 - 10
src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs → src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Platform;
 using SharpDX;
 using SharpDX;
 using SharpDX.Direct2D1;
 using SharpDX.Direct2D1;
 using SharpDX.Mathematics.Interop;
 using SharpDX.Mathematics.Interop;
@@ -15,7 +16,7 @@ namespace Avalonia.Direct2D1.Media
     /// <summary>
     /// <summary>
     /// Draws using Direct2D1.
     /// Draws using Direct2D1.
     /// </summary>
     /// </summary>
-    public class DrawingContext : IDrawingContextImpl, IDisposable
+    public class DrawingContextImpl : IDrawingContextImpl, IDisposable
     {
     {
         /// <summary>
         /// <summary>
         /// The Direct2D1 render target.
         /// The Direct2D1 render target.
@@ -28,11 +29,11 @@ namespace Avalonia.Direct2D1.Media
         private SharpDX.DirectWrite.Factory _directWriteFactory;
         private SharpDX.DirectWrite.Factory _directWriteFactory;
 
 
         /// <summary>
         /// <summary>
-        /// Initializes a new instance of the <see cref="DrawingContext"/> class.
+        /// Initializes a new instance of the <see cref="DrawingContextImpl"/> class.
         /// </summary>
         /// </summary>
         /// <param name="renderTarget">The render target to draw to.</param>
         /// <param name="renderTarget">The render target to draw to.</param>
         /// <param name="directWriteFactory">The DirectWrite factory.</param>
         /// <param name="directWriteFactory">The DirectWrite factory.</param>
-        public DrawingContext(
+        public DrawingContextImpl(
             SharpDX.Direct2D1.RenderTarget renderTarget,
             SharpDX.Direct2D1.RenderTarget renderTarget,
             SharpDX.DirectWrite.Factory directWriteFactory)
             SharpDX.DirectWrite.Factory directWriteFactory)
         {
         {
@@ -74,9 +75,9 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="opacity">The opacity to draw with.</param>
         /// <param name="opacity">The opacity to draw with.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
-        public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect)
+        public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
         {
         {
-            BitmapImpl impl = (BitmapImpl)source.PlatformImpl;
+            BitmapImpl impl = (BitmapImpl)source;
             Bitmap d2d = impl.GetDirect2DBitmap(_renderTarget);
             Bitmap d2d = impl.GetDirect2DBitmap(_renderTarget);
             _renderTarget.DrawBitmap(
             _renderTarget.DrawBitmap(
                 d2d,
                 d2d,
@@ -120,7 +121,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="geometry">The geometry.</param>
-        public void DrawGeometry(IBrush brush, Pen pen, Avalonia.Media.Geometry geometry)
+        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
         {
         {
             if (brush != null)
             if (brush != null)
             {
             {
@@ -128,7 +129,7 @@ namespace Avalonia.Direct2D1.Media
                 {
                 {
                     if (d2dBrush.PlatformBrush != null)
                     if (d2dBrush.PlatformBrush != null)
                     {
                     {
-                        var impl = (GeometryImpl)geometry.PlatformImpl;
+                        var impl = (GeometryImpl)geometry;
                         _renderTarget.FillGeometry(impl.Geometry, d2dBrush.PlatformBrush);
                         _renderTarget.FillGeometry(impl.Geometry, d2dBrush.PlatformBrush);
                     }
                     }
                 }
                 }
@@ -141,7 +142,7 @@ namespace Avalonia.Direct2D1.Media
                 {
                 {
                     if (d2dBrush.PlatformBrush != null)
                     if (d2dBrush.PlatformBrush != null)
                     {
                     {
-                        var impl = (GeometryImpl)geometry.PlatformImpl;
+                        var impl = (GeometryImpl)geometry;
                         _renderTarget.DrawGeometry(impl.Geometry, d2dBrush.PlatformBrush, (float)pen.Thickness, d2dStroke);
                         _renderTarget.DrawGeometry(impl.Geometry, d2dBrush.PlatformBrush, (float)pen.Thickness, d2dStroke);
                     }
                     }
                 }
                 }
@@ -187,11 +188,11 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="text">The text.</param>
         /// <param name="text">The text.</param>
-        public void DrawText(IBrush foreground, Point origin, FormattedText text)
+        public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
         {
         {
             if (!string.IsNullOrEmpty(text.Text))
             if (!string.IsNullOrEmpty(text.Text))
             {
             {
-                var impl = (FormattedTextImpl)text.PlatformImpl;
+                var impl = (FormattedTextImpl)text;
 
 
                 using (var brush = CreateBrush(foreground, impl.Measure()))
                 using (var brush = CreateBrush(foreground, impl.Measure()))
                 using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush))
                 using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush))

+ 4 - 0
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@@ -23,6 +23,8 @@ namespace Avalonia.Direct2D1.Media
         {
         {
             var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
             var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
 
 
+            Text = text;
+
             using (var format = new DWrite.TextFormat(
             using (var format = new DWrite.TextFormat(
                 factory,
                 factory,
                 fontFamily,
                 fontFamily,
@@ -58,6 +60,8 @@ namespace Avalonia.Direct2D1.Media
             }
             }
         }
         }
 
 
+        public string Text { get; }
+
         public DWrite.TextLayout TextLayout { get; }
         public DWrite.TextLayout TextLayout { get; }
 
 
         public void Dispose()
         public void Dispose()

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs

@@ -39,7 +39,7 @@ namespace Avalonia.Direct2D1.Media
             base.Dispose();
             base.Dispose();
         }
         }
 
 
-        public Avalonia.Media.DrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext();
+        public IDrawingContextImpl CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext();
         
         
     }
     }
 }
 }

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Direct2D1.Media
                 using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
                 using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
                 {
                 {
                     intermediate.Clear(null);
                     intermediate.Clear(null);
-                    helper.DrawIntermediate(ctx);
+                    helper.DrawIntermediate(new DrawingContext(ctx));
                 }
                 }
 
 
                 PlatformBrush = new BitmapBrush(
                 PlatformBrush = new BitmapBrush(

+ 4 - 2
src/Windows/Avalonia.Direct2D1/RenderTarget.cs

@@ -2,6 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 
 using System;
 using System;
+using Avalonia.Direct2D1.Media;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Win32.Interop;
 using Avalonia.Win32.Interop;
 using SharpDX;
 using SharpDX;
@@ -80,7 +82,7 @@ namespace Avalonia.Direct2D1
         /// Creates a drawing context for a rendering session.
         /// Creates a drawing context for a rendering session.
         /// </summary>
         /// </summary>
         /// <returns>An <see cref="Avalonia.Media.DrawingContext"/>.</returns>
         /// <returns>An <see cref="Avalonia.Media.DrawingContext"/>.</returns>
-        public DrawingContext CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext()
         {
         {
             var window = _renderTarget as WindowRenderTarget;
             var window = _renderTarget as WindowRenderTarget;
 
 
@@ -100,7 +102,7 @@ namespace Avalonia.Direct2D1
                 }
                 }
             }
             }
 
 
-            return new DrawingContext(new Media.DrawingContext(_renderTarget, DirectWriteFactory));
+            return new DrawingContextImpl(_renderTarget, DirectWriteFactory);
         }
         }
 
 
         public void Dispose()
         public void Dispose()

+ 6 - 3
tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject

@@ -17,10 +17,13 @@
   <DetectStackOverflow>true</DetectStackOverflow>
   <DetectStackOverflow>true</DetectStackOverflow>
   <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
   <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
   <DefaultTestTimeout>60000</DefaultTestTimeout>
   <DefaultTestTimeout>60000</DefaultTestTimeout>
-  <UseBuildConfiguration />
-  <UseBuildPlatform />
-  <ProxyProcessPath />
+  <UseBuildConfiguration></UseBuildConfiguration>
+  <UseBuildPlatform></UseBuildPlatform>
+  <ProxyProcessPath></ProxyProcessPath>
   <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
   <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
   <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
   <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
   <BuildProcessArchitecture>x86</BuildProcessArchitecture>
   <BuildProcessArchitecture>x86</BuildProcessArchitecture>
+  <IgnoredTests>
+    <AllTestsSelector />
+  </IgnoredTests>
 </ProjectConfiguration>
 </ProjectConfiguration>

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

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

+ 130 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -0,0 +1,130 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
+{
+    public class SceneBuilderTests
+    {
+        [Fact]
+        public void Should_Build_Initial_Scene()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Border border;
+                TextBlock textBlock;
+                var tree = new TestRoot
+                {
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Red,
+                        Child = textBlock = new TextBlock
+                        {
+                            Text = "Hello World",
+                        }
+                    }
+                };
+
+                var initial = new Scene(tree);
+                var result = SceneBuilder.Update(initial);
+
+                Assert.NotSame(initial, result);
+                Assert.Equal(1, result.Root.Children.Count);
+
+                var borderNode = (VisualNode)result.Root.Children[0];
+                Assert.Same(borderNode, result.FindNode(border));
+                Assert.Same(border, borderNode.Visual);
+                Assert.Equal(2, borderNode.Children.Count);
+
+                var backgroundNode = (RectangleNode)borderNode.Children[0];
+                Assert.Equal(Brushes.Red, backgroundNode.Brush);
+
+                var textBlockNode = (VisualNode)borderNode.Children[1];
+                Assert.Same(textBlockNode, result.FindNode(textBlock));
+                Assert.Same(textBlock, textBlockNode.Visual);
+                Assert.Equal(1, textBlockNode.Children.Count);
+
+                var textNode = (TextNode)textBlockNode.Children[0];
+                Assert.NotNull(textNode.Text);
+            }
+        }
+
+        [Fact]
+        public void Should_Update_Border_Background_Node()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Border border;
+                TextBlock textBlock;
+                var tree = new TestRoot
+                {
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Red,
+                        Child = textBlock = new TextBlock
+                        {
+                            Text = "Hello World",
+                        }
+                    }
+                };
+
+                var initial = SceneBuilder.Update(new Scene(tree));
+                var initialBackgroundNode = initial.FindNode(border).Children[0];
+                var initialTextNode = initial.FindNode(textBlock).Children[0];
+
+                Assert.NotNull(initialBackgroundNode);
+                Assert.NotNull(initialTextNode);
+
+                border.Background = Brushes.Green;
+                var result = SceneBuilder.Update(initial);
+
+                Assert.NotSame(initial, result);
+                var borderNode = (VisualNode)result.Root.Children[0];
+                Assert.Same(border, borderNode.Visual);
+
+                var backgroundNode = (RectangleNode)borderNode.Children[0];
+                Assert.NotSame(initialBackgroundNode, backgroundNode);
+                Assert.Equal(Brushes.Green, backgroundNode.Brush);
+
+                var textBlockNode = (VisualNode)borderNode.Children[1];
+                Assert.Same(textBlock, textBlockNode.Visual);
+
+                var textNode = (TextNode)textBlockNode.Children[0];
+                Assert.Same(initialTextNode, textNode);
+            }
+        }
+
+        [Fact]
+        public void Should_Update_When_Control_Removed()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Border border;
+                TextBlock textBlock;
+                var tree = new TestRoot
+                {
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Red,
+                        Child = textBlock = new TextBlock
+                        {
+                            Text = "Hello World",
+                        }
+                    }
+                };
+
+                var initial = SceneBuilder.Update(new Scene(tree));
+
+                border.Child = null;
+                var result = SceneBuilder.Update(initial);
+
+                Assert.NotSame(initial, result);
+                var borderNode = (VisualNode)result.Root.Children[0];
+                Assert.Equal(1, borderNode.Children.Count);
+            }
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/TestRoot.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Visuals.UnitTests
 
 
         public IRenderer Renderer
         public IRenderer Renderer
         {
         {
-            get { throw new NotImplementedException(); }
+            get { return AvaloniaLocator.Current.GetService<IRenderer>(); }
         }
         }
 
 
         public Point PointToClient(Point p)
         public Point PointToClient(Point p)