Parcourir la source

Merge branch 'master' into patch-3

Steven Kirk il y a 8 ans
Parent
commit
9e20b7ca02
100 fichiers modifiés avec 3935 ajouts et 378 suppressions
  1. 5 0
      .ncrunch/Avalonia.LinuxFramebuffer.v3.ncrunchproject
  2. 5 0
      .ncrunch/Avalonia.Visuals.UnitTests.net461.v3.ncrunchproject
  3. 5 0
      .ncrunch/Avalonia.Win32.Interop.v3.ncrunchproject
  4. 1 1
      build.cake
  5. 1 0
      samples/ControlCatalog/MainWindow.xaml.cs
  6. 1 1
      samples/interop/Direct3DInteropSample/Program.cs
  7. 0 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  8. 7 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  9. 3 2
      src/Avalonia.Base/Utilities/WeakObservable.cs
  10. 12 12
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  11. 7 0
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  12. 4 2
      src/Avalonia.Controls/TopLevel.cs
  13. 9 0
      src/Avalonia.Controls/Window.cs
  14. 2 0
      src/Avalonia.Controls/WindowBase.cs
  15. 2 1
      src/Avalonia.Themes.Default/CheckBox.xaml
  16. 1 0
      src/Avalonia.Themes.Default/DropDownItem.xaml
  17. 3 3
      src/Avalonia.Themes.Default/ListBox.xaml
  18. 1 0
      src/Avalonia.Themes.Default/ListBoxItem.xaml
  19. 1 0
      src/Avalonia.Themes.Default/MenuItem.xaml
  20. 2 1
      src/Avalonia.Themes.Default/RadioButton.xaml
  21. 2 1
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  22. 1 0
      src/Avalonia.Themes.Default/TabStripItem.xaml
  23. 3 3
      src/Avalonia.Themes.Default/TreeView.xaml
  24. 2 1
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  25. 51 0
      src/Avalonia.Visuals/Media/BrushExtensions.cs
  26. 0 0
      src/Avalonia.Visuals/Media/IImageBrush.cs
  27. 10 2
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  28. 7 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  29. 34 0
      src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs
  30. 430 0
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  31. 88 0
      src/Avalonia.Visuals/Rendering/DirtyRects.cs
  32. 107 0
      src/Avalonia.Visuals/Rendering/DirtyVisuals.cs
  33. 51 0
      src/Avalonia.Visuals/Rendering/DisplayDirtyRect.cs
  34. 62 0
      src/Avalonia.Visuals/Rendering/DisplayDirtyRects.cs
  35. 11 0
      src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs
  36. 5 0
      src/Avalonia.Visuals/Rendering/IRenderRoot.cs
  37. 10 0
      src/Avalonia.Visuals/Rendering/IRenderer.cs
  38. 0 18
      src/Avalonia.Visuals/Rendering/IRendererFactory.cs
  39. 10 7
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  40. 47 0
      src/Avalonia.Visuals/Rendering/RenderLayer.cs
  41. 63 0
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  42. 31 0
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  43. 64 0
      src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
  44. 392 0
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  45. 64 0
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs
  46. 101 0
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  47. 36 0
      src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs
  48. 24 0
      src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs
  49. 94 0
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  50. 94 0
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  51. 95 0
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  52. 80 0
      src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs
  53. 64 0
      src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs
  54. 116 0
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  55. 184 0
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  56. 384 0
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  57. 75 0
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs
  58. 199 0
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
  59. 96 0
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  60. 285 0
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  61. 11 0
      src/Avalonia.Visuals/Vector.cs
  62. 24 0
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  63. 0 26
      src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject
  64. 1 0
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  65. 3 2
      src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs
  66. 1 7
      src/Gtk/Avalonia.Gtk/GtkPlatform.cs
  67. 6 0
      src/Gtk/Avalonia.Gtk/TopLevelImpl.cs
  68. 1 7
      src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs
  69. 6 0
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  70. 6 0
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  71. 0 1
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  72. 4 4
      src/Markup/Avalonia.Markup/Data/IndexerNode.cs
  73. 9 1
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  74. 4 3
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  75. 3 3
      src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  76. 1 1
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  77. 1 0
      src/Shared/SharedAssemblyInfo.cs
  78. 0 26
      src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject
  79. 0 26
      src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject
  80. 0 172
      src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs
  81. 10 10
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  82. 14 5
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  83. 1 0
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  84. 1 2
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  85. 2 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  86. 11 0
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  87. 1 1
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  88. 4 4
      src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
  89. 6 0
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  90. 1 1
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  91. 17 11
      src/Windows/Avalonia.Win32/Win32Platform.cs
  92. 10 0
      src/Windows/Avalonia.Win32/WindowImpl.cs
  93. 7 1
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  94. 0 1
      src/iOS/Avalonia.iOS/iOSPlatform.cs
  95. 1 0
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  96. 1 0
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs
  97. 56 2
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  98. 53 0
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  99. 29 0
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  100. 85 0
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

+ 5 - 0
.ncrunch/Avalonia.LinuxFramebuffer.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/Avalonia.Visuals.UnitTests.net461.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/Avalonia.Win32.Interop.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
+  </Settings>
+</ProjectConfiguration>

+ 1 - 1
build.cake

@@ -193,7 +193,7 @@ Task("Run-Net-Core-Unit-Tests")
 Task("Run-Unit-Tests")
     .IsDependentOn("Run-Net-Core-Unit-Tests")
     .IsDependentOn("Build")
-    .IsDependentOn("Run-Leak-Tests")
+    //.IsDependentOn("Run-Leak-Tests")
     .WithCriteria(() => !parameters.SkipTests)
     .Does(() =>
 {

+ 1 - 0
samples/ControlCatalog/MainWindow.xaml.cs

@@ -10,6 +10,7 @@ namespace ControlCatalog
         {
             this.InitializeComponent();
             this.AttachDevTools();
+            Renderer.DrawDirtyRects = Renderer.DrawFps = true;
         }
 
         private void InitializeComponent()

+ 1 - 1
samples/interop/Direct3DInteropSample/Program.cs

@@ -11,7 +11,7 @@ namespace Direct3DInteropSample
     {
         static void Main(string[] args)
         {
-            AppBuilder.Configure<App>().UseWin32().UseDirect2D1().Start<MainWindow>();
+            AppBuilder.Configure<App>().UseWin32(deferredRendering: false).UseDirect2D1().Start<MainWindow>();
         }
     }
 }

+ 0 - 2
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -52,13 +52,11 @@ namespace Avalonia.Android
                 .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
                 .Bind<IPlatformSettings>().ToConstant(Instance)
-                .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
                 .Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
                 .Bind<IWindowingPlatform>().ToConstant(Instance)
                 .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
                 .Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
-
                 .Bind<IAssetLoader>().ToConstant(new AssetLoader(app.GetType().Assembly));
 
             SkiaPlatform.Initialize();

+ 7 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -13,6 +13,7 @@ using System.Reactive.Disposables;
 using Avalonia.Android.Platform.Input;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Rendering;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
@@ -85,7 +86,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public IPlatformHandle Handle => _view;
 
         public IEnumerable<object> Surfaces => new object[] {this};
-        
+
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            return new ImmediateRenderer(root);
+        }
+
         public virtual void Hide()
         {
             _view.Visibility = ViewStates.Invisible;

+ 3 - 2
src/Avalonia.Base/Utilities/WeakObservable.cs

@@ -16,12 +16,13 @@ namespace Avalonia.Utilities
         /// Converts a .NET event conforming to the standard .NET event pattern into an observable
         /// sequence, subscribing weakly.
         /// </summary>
+        /// <typeparam name="TTarget">The type of target.</typeparam>
         /// <typeparam name="TEventArgs">The type of the event args.</typeparam>
         /// <param name="target">Object instance that exposes the event to convert.</param>
         /// <param name="eventName">Name of the event to convert.</param>
         /// <returns></returns>
-        public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TEventArgs>(
-            object target, 
+        public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
+            TTarget target, 
             string eventName)
             where TEventArgs : EventArgs
         {

+ 12 - 12
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@@ -17,22 +17,23 @@ namespace Avalonia.Utilities
         /// <summary>
         /// Subscribes to an event on an object using a weak subscription.
         /// </summary>
-        /// <typeparam name="T">The type of the event arguments.</typeparam>
+        /// <typeparam name="TTarget">The type of the target.</typeparam>
+        /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
         /// <param name="target">The event source.</param>
         /// <param name="eventName">The name of the event.</param>
         /// <param name="subscriber">The subscriber.</param>
-        public static void Subscribe<T>(object target, string eventName, IWeakSubscriber<T> subscriber)
-            where T : EventArgs
+        public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber)
+            where TEventArgs : EventArgs
         {
-            var dic = SubscriptionTypeStorage<T>.Subscribers.GetOrCreateValue(target);
-            Subscription<T> sub;
+            var dic = SubscriptionTypeStorage<TEventArgs>.Subscribers.GetOrCreateValue(target);
+            Subscription<TEventArgs> sub;
 
             if (!dic.TryGetValue(eventName, out sub))
             {
-                dic[eventName] = sub = new Subscription<T>(dic, target, eventName);
+                dic[eventName] = sub = new Subscription<TEventArgs>(dic, typeof(TTarget), target, eventName);
             }
 
-            sub.Add(new WeakReference<IWeakSubscriber<T>>(subscriber));
+            sub.Add(new WeakReference<IWeakSubscriber<TEventArgs>>(subscriber));
         }
 
         /// <summary>
@@ -84,19 +85,18 @@ namespace Avalonia.Utilities
             private WeakReference<IWeakSubscriber<T>>[] _data = new WeakReference<IWeakSubscriber<T>>[16];
             private int _count = 0;
 
-            public Subscription(SubscriptionDic<T> sdic, object target, string eventName)
+            public Subscription(SubscriptionDic<T> sdic, Type targetType, object target, string eventName)
             {
                 _sdic = sdic;
                 _target = target;
                 _eventName = eventName;
-                var t = target.GetType();
                 Dictionary<string, EventInfo> evDic;
-                if (!Accessors.TryGetValue(t, out evDic))
-                    Accessors[t] = evDic = new Dictionary<string, EventInfo>();
+                if (!Accessors.TryGetValue(targetType, out evDic))
+                    Accessors[targetType] = evDic = new Dictionary<string, EventInfo>();
 
                 if (!evDic.TryGetValue(eventName, out _info))
                 {
-                    var ev = t.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName);
+                    var ev = targetType.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName);
 
                     if (ev == null)
                     {

+ 7 - 0
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Rendering;
 using JetBrains.Annotations;
 
 namespace Avalonia.Platform
@@ -60,6 +61,12 @@ namespace Avalonia.Platform
         /// </summary>
         Action<double> ScalingChanged { get; set; }
 
+        /// <summary>
+        /// Creates a new renderer for the toplevel.
+        /// </summary>
+        /// <param name="root">The toplevel.</param>
+        IRenderer CreateRenderer(IRenderRoot root);
+
         /// <summary>
         /// Invalidates a rect on the toplevel.
         /// </summary>

+ 4 - 2
src/Avalonia.Controls/TopLevel.cs

@@ -90,8 +90,7 @@ namespace Avalonia.Controls
             _renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
 
             var renderLoop = TryGetService<IRenderLoop>(dependencyResolver);
-            var rendererFactory = TryGetService<IRendererFactory>(dependencyResolver);
-            Renderer = rendererFactory?.CreateRenderer(this, renderLoop);
+            Renderer = impl.CreateRenderer(this);
 
             impl.SetInputRoot(this);
 
@@ -181,6 +180,9 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1;
 
+        /// <inheritdoc/>
+        double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1;
+
         IStyleHost IStyleHost.StylingParent
         {
             get { return AvaloniaLocator.Current.GetService<IGlobalStyles>(); }

+ 9 - 0
src/Avalonia.Controls/Window.cs

@@ -225,8 +225,14 @@ namespace Avalonia.Controls
         /// </summary>
         public override void Hide()
         {
+            if (!IsVisible)
+            {
+                return;
+            }
+
             using (BeginAutoSizing())
             {
+                Renderer?.Stop();
                 PlatformImpl?.Hide();
             }
 
@@ -252,6 +258,7 @@ namespace Avalonia.Controls
             using (BeginAutoSizing())
             {
                 PlatformImpl?.Show();
+                Renderer?.Start();
             }
         }
 
@@ -297,6 +304,8 @@ namespace Avalonia.Controls
                 var modal = PlatformImpl?.ShowDialog();
                 var result = new TaskCompletionSource<TResult>();
 
+                Renderer?.Start();
+
                 Observable.FromEventPattern<EventHandler, EventArgs>(
                     x => this.Closed += x,
                     x => this.Closed -= x)

+ 2 - 0
src/Avalonia.Controls/WindowBase.cs

@@ -117,6 +117,7 @@ namespace Avalonia.Controls
 
             try
             {
+                Renderer?.Stop();
                 PlatformImpl?.Hide();
                 IsVisible = false;
             }
@@ -145,6 +146,7 @@ namespace Avalonia.Controls
                 }
 
                 PlatformImpl?.Show();
+                Renderer?.Start();
             }
             finally
             {

+ 2 - 1
src/Avalonia.Themes.Default/CheckBox.xaml

@@ -1,10 +1,11 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="CheckBox">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
     <Setter Property="Template">
       <ControlTemplate>
-        <Grid ColumnDefinitions="Auto,*">
+        <Grid ColumnDefinitions="Auto,*" Background="{TemplateBinding Background}">
           <Border Name="border"
                   BorderBrush="{TemplateBinding BorderBrush}"
                   BorderThickness="{TemplateBinding BorderThickness}"

+ 1 - 0
src/Avalonia.Themes.Default/DropDownItem.xaml

@@ -1,5 +1,6 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="DropDownItem">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="Padding" Value="2"/>
     <Setter Property="HorizontalAlignment" Value="Stretch"/>
     <Setter Property="HorizontalContentAlignment" Value="Left"/>

+ 3 - 3
src/Avalonia.Themes.Default/ListBox.xaml

@@ -1,13 +1,13 @@
 <Style xmlns="https://github.com/avaloniaui" Selector="ListBox">
+  <Setter Property="Background" Value="{StyleResource ThemeBackgroundBrush}"/>
   <Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
   <Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
   <Setter Property="Padding" Value="4"/>
   <Setter Property="Template">
     <ControlTemplate>
-      <Border Background="{TemplateBinding Background}"
-              BorderBrush="{TemplateBinding BorderBrush}"
+      <Border BorderBrush="{TemplateBinding BorderBrush}"
               BorderThickness="{TemplateBinding BorderThickness}">
-        <ScrollViewer Name="PART_ScrollViewer">
+        <ScrollViewer Name="PART_ScrollViewer" Background="{TemplateBinding Background}">
           <ItemsPresenter Name="PART_ItemsPresenter"
                           Items="{TemplateBinding Items}"
                           ItemsPanel="{TemplateBinding ItemsPanel}"

+ 1 - 0
src/Avalonia.Themes.Default/ListBoxItem.xaml

@@ -1,5 +1,6 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="ListBoxItem">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="Template">
       <ControlTemplate>
         <ContentPresenter Name="PART_ContentPresenter"

+ 1 - 0
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -2,6 +2,7 @@
         xmlns:sys="clr-namespace:System;assembly=mscorlib">
   
   <Style Selector="MenuItem">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderThickness" Value="1"/>
     <Setter Property="Padding" Value="6,0"/>
     <Setter Property="Template">

+ 2 - 1
src/Avalonia.Themes.Default/RadioButton.xaml

@@ -1,10 +1,11 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="RadioButton">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
     <Setter Property="Template">
       <ControlTemplate>
-        <Grid ColumnDefinitions="Auto,*">
+        <Grid ColumnDefinitions="Auto,*" Background="{TemplateBinding Background}">
           <Ellipse Name="border"
                    Stroke="{TemplateBinding BorderBrush}"
                    StrokeThickness="{TemplateBinding BorderThickness}"

+ 2 - 1
src/Avalonia.Themes.Default/ScrollViewer.xaml

@@ -1,8 +1,9 @@
 <Style xmlns="https://github.com/avaloniaui" Selector="ScrollViewer">
   <Setter Property="Template">
     <ControlTemplate>
-      <Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto" Background="{TemplateBinding Background}">
+      <Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
         <ScrollContentPresenter Name="PART_ContentPresenter"
+                                Background="{TemplateBinding Background}"
                                 Content="{TemplateBinding Content}"
                                 Extent="{TemplateBinding Path=Extent, Mode=TwoWay}"
                                 Margin="{TemplateBinding Padding}"

+ 1 - 0
src/Avalonia.Themes.Default/TabStripItem.xaml

@@ -1,5 +1,6 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="TabStripItem">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="FontSize" Value="{StyleResource FontSizeLarge}"/>
     <Setter Property="Foreground" Value="{StyleResource ThemeForegroundLightBrush}"/>
     <Setter Property="Template">

+ 3 - 3
src/Avalonia.Themes.Default/TreeView.xaml

@@ -1,13 +1,13 @@
 <Style xmlns="https://github.com/avaloniaui" Selector="TreeView">
+  <Setter Property="Background" Value="Transparent"/>
   <Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
   <Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
   <Setter Property="Padding" Value="4"/>
   <Setter Property="Template">
     <ControlTemplate>
-      <Border Background="{TemplateBinding Background}"
-              BorderBrush="{TemplateBinding BorderBrush}"
+      <Border BorderBrush="{TemplateBinding BorderBrush}"
               BorderThickness="{TemplateBinding BorderThickness}">
-        <ScrollViewer CanScrollHorizontally="True">
+        <ScrollViewer CanScrollHorizontally="True" Background="{TemplateBinding Background}">
           <ItemsPresenter Name="PART_ItemsPresenter"
                           Items="{TemplateBinding Items}"
                           ItemsPanel="{TemplateBinding ItemsPanel}"

+ 2 - 1
src/Avalonia.Themes.Default/TreeViewItem.xaml

@@ -31,7 +31,8 @@
   <Style Selector="TreeViewItem /template/ ToggleButton#expander">
     <Setter Property="Template">
       <ControlTemplate>
-        <Border Width="14"
+        <Border Background="Transparent"
+                Width="14"
                 Height="12"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center">

+ 51 - 0
src/Avalonia.Visuals/Media/BrushExtensions.cs

@@ -0,0 +1,51 @@
+using System;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Extension methods for brush classes.
+    /// </summary>
+    public static class BrushExtensions
+    {
+        /// <summary>
+        /// Converts a brush to an immutable brush.
+        /// </summary>
+        /// <param name="brush">The brush.</param>
+        /// <returns>
+        /// The result of calling <see cref="IMutableBrush.ToImmutable"/> if the brush is mutable,
+        /// otherwise <paramref name="brush"/>.
+        /// </returns>
+        public static IBrush ToImmutable(this IBrush brush)
+        {
+            Contract.Requires<ArgumentNullException>(brush != null);
+
+            return (brush as IMutableBrush)?.ToImmutable() ?? brush;
+        }
+
+        /// <summary>
+        /// Converts a pen to a pen with an immutable brush
+        /// </summary>
+        /// <param name="pen">The pen.</param>
+        /// <returns>
+        /// A copy of the pen with an immutable brush, or <paramref name="pen"/> if the pen's brush
+        /// is already immutable or null.
+        /// </returns>
+        public static Pen ToImmutable(this Pen pen)
+        {
+            Contract.Requires<ArgumentNullException>(pen != null);
+
+            var brush = pen?.Brush?.ToImmutable();
+            return pen == null || ReferenceEquals(pen?.Brush, brush) ?
+                pen :
+                new Pen(
+                    brush,
+                    thickness: pen.Thickness,
+                    dashStyle: pen.DashStyle,
+                    dashCap: pen.DashCap,
+                    startLineCap: pen.StartLineCap,
+                    endLineCap: pen.EndLineCap,
+                    lineJoin: pen.LineJoin,
+                    miterLimit: pen.MiterLimit);
+        }
+    }
+}

+ 0 - 0
src/Avalonia.Visuals/Media/Imaging/IImageBrush.cs → src/Avalonia.Visuals/Media/IImageBrush.cs


+ 10 - 2
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -3,7 +3,6 @@
 
 using System;
 using Avalonia.Media;
-using Avalonia.Platform;
 
 namespace Avalonia.Platform
 {
@@ -32,6 +31,15 @@ namespace Avalonia.Platform
         /// <param name="destRect">The rect in the output to draw to.</param>
         void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect);
 
+        /// <summary>
+        /// Draws a bitmap image.
+        /// </summary>
+        /// <param name="source">The bitmap image.</param>
+        /// <param name="opacityMask">The opacity mask to draw with.</param>
+        /// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
+        /// <param name="destRect">The rect in the output to draw to.</param>
+        void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
+
         /// <summary>
         /// Draws a line.
         /// </summary>
@@ -100,4 +108,4 @@ namespace Avalonia.Platform
 
         void PopGeometryClip();
     }
-}
+}

+ 7 - 1
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -2,8 +2,14 @@
 // 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", "Avalonia.Media")]
+
+[assembly: InternalsVisibleTo("Avalonia.Cairo.RenderTests")]
+[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
+[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]

+ 34 - 0
src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs

@@ -0,0 +1,34 @@
+using System;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class DefaultRenderLayerFactory : IRenderLayerFactory
+    {
+        private IPlatformRenderInterface _renderInterface;
+
+        public DefaultRenderLayerFactory()
+            : this(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>())
+        {
+        }
+
+        public DefaultRenderLayerFactory(IPlatformRenderInterface renderInterface)
+        {
+            _renderInterface = renderInterface;
+        }
+
+        public IRenderTargetBitmapImpl CreateLayer(
+            IVisual layerRoot,
+            Size size,
+            double dpiX,
+            double dpiY)
+        {
+            return _renderInterface.CreateRenderTargetBitmap(
+                (int)Math.Ceiling(size.Width),
+                (int)Math.Ceiling(size.Height),
+                dpiX,
+                dpiY);
+        }
+    }
+}

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

@@ -0,0 +1,430 @@
+// 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;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using System.Collections.Generic;
+using System.IO;
+using Avalonia.Media.Immutable;
+using System.Threading;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// A renderer which renders the state of the visual tree to an intermediate scene graph
+    /// representation which is then rendered on a rendering thread.
+    /// </summary>
+    public class DeferredRenderer : RendererBase, IRenderer, IVisualBrushRenderer
+    {
+        private readonly IDispatcher _dispatcher;
+        private readonly IRenderLoop _renderLoop;
+        private readonly IVisual _root;
+        private readonly ISceneBuilder _sceneBuilder;
+        private readonly RenderLayers _layers;
+        private readonly IRenderLayerFactory _layerFactory;
+
+        private bool _running;
+        private Scene _scene;
+        private IRenderTarget _renderTarget;
+        private DirtyVisuals _dirty;
+        private IRenderTargetBitmapImpl _overlay;
+        private bool _updateQueued;
+        private object _rendering = new object();
+        private int _lastSceneId = -1;
+        private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
+        private IDrawOperation _currentDraw;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
+        /// </summary>
+        /// <param name="root">The control to render.</param>
+        /// <param name="renderLoop">The render loop.</param>
+        /// <param name="sceneBuilder">The scene builder to use. Optional.</param>
+        /// <param name="layerFactory">The layer factory to use. Optional.</param>
+        /// <param name="dispatcher">The dispatcher to use. Optional.</param>
+        public DeferredRenderer(
+            IRenderRoot root,
+            IRenderLoop renderLoop,
+            ISceneBuilder sceneBuilder = null,
+            IRenderLayerFactory layerFactory = null,
+            IDispatcher dispatcher = null)
+        {
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            _dispatcher = dispatcher ?? Dispatcher.UIThread;
+            _root = root;
+            _sceneBuilder = sceneBuilder ?? new SceneBuilder();
+            _scene = new Scene(root);
+            _layerFactory = layerFactory ?? new DefaultRenderLayerFactory();
+            _layers = new RenderLayers(_layerFactory);
+            _renderLoop = renderLoop;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
+        /// </summary>
+        /// <param name="root">The control to render.</param>
+        /// <param name="renderTarget">The render target.</param>
+        /// <param name="sceneBuilder">The scene builder to use. Optional.</param>
+        /// <param name="layerFactory">The layer factory to use. Optional.</param>
+        /// <remarks>
+        /// This constructor is intended to be used for unit testing.
+        /// </remarks>
+        public DeferredRenderer(
+            IVisual root,
+            IRenderTarget renderTarget,
+            ISceneBuilder sceneBuilder = null,
+            IRenderLayerFactory layerFactory = null)
+        {
+            Contract.Requires<ArgumentNullException>(root != null);
+            Contract.Requires<ArgumentNullException>(renderTarget != null);
+
+            _root = root;
+            _renderTarget = renderTarget;
+            _sceneBuilder = sceneBuilder ?? new SceneBuilder();
+            _scene = new Scene(root);
+            _layerFactory = layerFactory ?? new DefaultRenderLayerFactory();
+            _layers = new RenderLayers(_layerFactory);
+        }
+
+        /// <inheritdoc/>
+        public bool DrawFps { get; set; }
+
+        /// <inheritdoc/>
+        public bool DrawDirtyRects { get; set; }
+
+        /// <summary>
+        /// Gets or sets a path to which rendered frame should be rendered for debugging.
+        /// </summary>
+        public string DebugFramesPath { get; set; }
+
+        /// <inheritdoc/>
+        public void AddDirty(IVisual visual)
+        {
+            _dirty?.Add(visual);
+        }
+
+        /// <summary>
+        /// Disposes of the renderer and detaches from the render loop.
+        /// </summary>
+        public void Dispose() => Stop();
+
+        /// <inheritdoc/>
+        public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
+        {
+            if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
+            {
+                // When unit testing the renderLoop may be null, so update the scene manually.
+                UpdateScene();
+            }
+
+            return _scene.HitTest(p, filter);
+        }
+
+        /// <inheritdoc/>
+        public void Paint(Rect rect)
+        {
+        }
+
+        /// <inheritdoc/>
+        public void Resized(Size size)
+        {
+        }
+
+        /// <inheritdoc/>
+        public void Start()
+        {
+            if (!_running && _renderLoop != null)
+            {
+                _renderLoop.Tick += OnRenderLoopTick;
+                _running = true;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void Stop()
+        {
+            if (_running && _renderLoop != null)
+            {
+                _renderLoop.Tick -= OnRenderLoopTick;
+                _running = false;
+            }
+        }
+
+        /// <inheritdoc/>
+        Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
+        {
+            return (_currentDraw as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
+        }
+
+        /// <inheritdoc/>
+        void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
+        {
+            var childScene = (_currentDraw as BrushDrawOperation)?.ChildScenes?[brush.Visual];
+
+            if (childScene != null)
+            {
+                Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size));
+            }
+        }
+
+        internal void UnitTestUpdateScene() => UpdateScene();
+
+        internal void UnitTestRender() => Render(_scene);
+
+        private void Render(Scene scene)
+        {
+            _dirtyRectsDisplay.Tick();
+
+            if (scene.Size != Size.Empty)
+            {
+                if (scene.Generation != _lastSceneId)
+                {
+                    _layers.Update(scene);
+                    RenderToLayers(scene);
+
+                    if (DebugFramesPath != null)
+                    {
+                        SaveDebugFrames(scene.Generation);
+                    }
+
+                    _lastSceneId = scene.Generation;
+                }
+
+                RenderOverlay(scene);
+                RenderComposite(scene);
+            }
+        }
+
+        private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds)
+        {
+            if (layer == null || node.LayerRoot == layer)
+            {
+                clipBounds = node.ClipBounds.Intersect(clipBounds);
+
+                if (!clipBounds.IsEmpty)
+                {
+                    node.BeginRender(context);
+
+                    foreach (var operation in node.DrawOperations)
+                    {
+                        _currentDraw = operation;
+                        operation.Render(context);
+                        _currentDraw = null;
+                    }
+
+                    foreach (var child in node.Children)
+                    {
+                        Render(context, (VisualNode)child, layer, clipBounds);
+                    }
+
+                    node.EndRender(context);
+                }
+            }
+        }
+
+        private void RenderToLayers(Scene scene)
+        {
+            if (scene.Layers.HasDirty)
+            {
+                foreach (var layer in scene.Layers)
+                {
+                    var renderTarget = _layers[layer.LayerRoot].Bitmap;
+                    var node = (VisualNode)scene.FindNode(layer.LayerRoot);
+
+                    if (node != null)
+                    {
+                        using (var context = renderTarget.CreateDrawingContext(this))
+                        {
+                            foreach (var rect in layer.Dirty)
+                            {
+                                context.Transform = Matrix.Identity;
+                                context.PushClip(rect);
+                                context.Clear(Colors.Transparent);
+                                Render(context, node, layer.LayerRoot, rect);
+                                context.PopClip();
+
+                                if (DrawDirtyRects)
+                                {
+                                    _dirtyRectsDisplay.Add(rect);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private void RenderOverlay(Scene scene)
+        {
+            if (DrawDirtyRects)
+            {
+                var overlay = GetOverlay(scene.Size, scene.Scaling);
+
+                using (var context = overlay.CreateDrawingContext(this))
+                {
+                    context.Clear(Colors.Transparent);
+                    RenderDirtyRects(context);
+                }
+            }
+            else
+            {
+                _overlay?.Dispose();
+                _overlay = null;
+            }
+        }
+
+        private void RenderDirtyRects(IDrawingContextImpl context)
+        {
+            foreach (var r in _dirtyRectsDisplay)
+            {
+                var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity);
+                context.FillRectangle(brush, r.Rect);
+            }
+        }
+
+        private void RenderComposite(Scene scene)
+        {
+            try
+            {
+                if (_renderTarget == null)
+                {
+                    _renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
+                }
+
+                using (var context = _renderTarget.CreateDrawingContext(this))
+                {
+                    var clientRect = new Rect(scene.Size);
+
+                    foreach (var layer in scene.Layers)
+                    {
+                        var bitmap = _layers[layer.LayerRoot].Bitmap;
+                        var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
+
+                        if (layer.GeometryClip != null)
+                        {
+                            context.PushGeometryClip(layer.GeometryClip);
+                        }
+
+                        if (layer.OpacityMask == null)
+                        {
+                            context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect);
+                        }
+                        else
+                        {
+                            context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect);
+                        }
+
+                        if (layer.GeometryClip != null)
+                        {
+                            context.PopGeometryClip();
+                        }
+                    }
+
+                    if (_overlay != null)
+                    {
+                        var sourceRect = new Rect(0, 0, _overlay.PixelWidth, _overlay.PixelHeight);
+                        context.DrawImage(_overlay, 0.5, sourceRect, clientRect);
+                    }
+
+                    if (DrawFps)
+                    {
+                        RenderFps(context, clientRect, true);
+                    }
+                }
+            }
+            catch (RenderTargetCorruptedException ex)
+            {
+                Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
+                _renderTarget?.Dispose();
+                _renderTarget = null;
+            }
+        }
+
+        private void UpdateScene()
+        {
+            Dispatcher.UIThread.VerifyAccess();
+
+            try
+            {
+                var scene = _scene.Clone();
+
+                if (_dirty == null)
+                {
+                    _dirty = new DirtyVisuals();
+                    _sceneBuilder.UpdateAll(scene);
+                }
+                else if (_dirty.Count > 0)
+                {
+                    foreach (var visual in _dirty)
+                    {
+                        _sceneBuilder.Update(scene, visual);
+                    }
+                }
+
+                Interlocked.Exchange(ref _scene, scene);
+
+                _dirty.Clear();
+                (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
+            }
+            finally
+            {
+                _updateQueued = false;
+            }
+        }
+
+        private void OnRenderLoopTick(object sender, EventArgs e)
+        {
+            if (Monitor.TryEnter(_rendering))
+            {
+                try
+                {
+                    if (!_updateQueued && (_dirty == null || _dirty.Count > 0))
+                    {
+                        _updateQueued = true;
+                        _dispatcher.InvokeAsync(UpdateScene, DispatcherPriority.Render);
+                    }
+
+                    Scene scene = null;
+                    Interlocked.Exchange(ref scene, _scene);
+                    Render(scene);
+                }
+                catch { }
+                finally
+                {
+                    Monitor.Exit(_rendering);
+                }
+            }
+        }
+
+        private IRenderTargetBitmapImpl GetOverlay(Size size, double scaling)
+        {
+            size = new Size(size.Width * scaling, size.Height * scaling);
+
+            if (_overlay == null ||
+                _overlay.PixelWidth != size.Width ||
+                _overlay.PixelHeight != size.Height)
+            {
+                _overlay?.Dispose();
+                _overlay = _layerFactory.CreateLayer(null, size, 96 * scaling, 96 * scaling);
+            }
+
+            return _overlay;
+        }
+
+        private void SaveDebugFrames(int id)
+        {
+            var index = 0;
+
+            foreach (var layer in _layers)
+            {
+                var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png");
+                layer.Bitmap.Save(fileName);
+            }
+        }
+    }
+}

+ 88 - 0
src/Avalonia.Visuals/Rendering/DirtyRects.cs

@@ -0,0 +1,88 @@
+// 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;
+using System.Collections.Generic;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// Tracks dirty rectangles.
+    /// </summary>
+    internal class DirtyRects : IEnumerable<Rect>
+    {
+        private List<Rect> _rects = new List<Rect>();
+
+        public bool IsEmpty => _rects.Count == 0;
+
+        /// <summary>
+        /// Adds a dirty rectangle, extending an existing dirty rectangle if it intersects.
+        /// </summary>
+        /// <param name="rect">The dirt rectangle.</param>
+        /// <remarks>
+        /// We probably want to do this more intellegently because:
+        /// - Adding e.g. the top left quarter of a scene and the bottom left quarter of a scene
+        ///   will cause the whole scene to be invalidated if they overlap by a single pixel
+        /// - Adding two adjacent rectangles that don't overlap will not cause them to be 
+        /// coalesced
+        /// - It only coaleces the first intersecting rectangle found - one needs to
+        ///  call <see cref="Coalesce"/> at the end of the draw cycle to coalesce the rest.
+        /// </remarks>
+        public void Add(Rect rect)
+        {
+            if (!rect.IsEmpty)
+            {
+                for (var i = 0; i < _rects.Count; ++i)
+                {
+                    var r = _rects[i];
+
+                    if (r.Intersects(rect))
+                    {
+                        _rects[i] = r.Union(rect);
+                        return;
+                    }
+                }
+
+                _rects.Add(rect);
+            }
+        }
+
+        /// <summary>
+        /// Works around our flimsy dirt-rect coalescing algorithm.
+        /// </summary>
+        /// <remarks>
+        /// See the comments in <see cref="Add(Rect)"/>.
+        /// </remarks>
+        public void Coalesce()
+        {
+            for (var i = _rects.Count - 1; i >= 0; --i)
+            {
+                var a = _rects[i];
+
+                for (var j = 0; j < i; ++j)
+                {
+                    var b = _rects[j];
+
+                    if (i < _rects.Count && a.Intersects(b))
+                    {
+                        _rects[i] = _rects[i].Union(b);
+                        _rects.RemoveAt(i);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the dirty rectangles.
+        /// </summary>
+        /// <returns>A collection of dirty rectangles</returns>
+        public IEnumerator<Rect> GetEnumerator() => _rects.GetEnumerator();
+
+        /// <summary>
+        /// Gets the dirty rectangles.
+        /// </summary>
+        /// <returns>A collection of dirty rectangles</returns>
+        IEnumerator IEnumerable.GetEnumerator() => _rects.GetEnumerator();
+    }
+}

+ 107 - 0
src/Avalonia.Visuals/Rendering/DirtyVisuals.cs

@@ -0,0 +1,107 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// Stores a list of dirty visuals for an <see cref="IRenderer"/>.
+    /// </summary>
+    /// <remarks>
+    /// This class stores the dirty visuals for a scene, ordered by their distance to the root
+    /// visual. TODO: We probably want to put an upper limit on the number of visuals that can be
+    /// stored and if we reach that limit, assume all visuals are dirty.
+    /// </remarks>
+    internal class DirtyVisuals : IEnumerable<IVisual>
+    {
+        private SortedDictionary<int, List<IVisual>> _inner = new SortedDictionary<int, List<IVisual>>();
+        private Dictionary<IVisual, int> _index = new Dictionary<IVisual, int>();
+
+        /// <summary>
+        /// Gets the number of dirty visuals.
+        /// </summary>
+        public int Count => _index.Count;
+
+        /// <summary>
+        /// Adds a visual to the dirty list.
+        /// </summary>
+        /// <param name="visual">The dirty visual.</param>
+        public void Add(IVisual visual)
+        {
+            var distance = visual.CalculateDistanceFromAncestor(visual.VisualRoot);
+            int existingDistance;
+
+            if (_index.TryGetValue(visual, out existingDistance))
+            {
+                if (distance == existingDistance)
+                {
+                    return;
+                }
+
+                _inner[existingDistance].Remove(visual);
+                _index.Remove(visual);
+            }
+
+            List<IVisual> list;
+
+            if (!_inner.TryGetValue(distance, out list))
+            {
+                list = new List<IVisual>();
+                _inner.Add(distance, list);
+            }
+
+            list.Add(visual);
+            _index.Add(visual, distance);
+        }
+
+        /// <summary>
+        /// Clears the list.
+        /// </summary>
+        public void Clear()
+        {
+            _inner.Clear();
+            _index.Clear();
+        }
+
+        /// <summary>
+        /// Removes a visual from the dirty list.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <returns>True if the visual was present in the list; otherwise false.</returns>
+        public bool Remove(IVisual visual)
+        {
+            int distance;
+
+            if (_index.TryGetValue(visual, out distance))
+            {
+                _inner[distance].Remove(visual);
+                _index.Remove(visual);
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Gets the dirty visuals, in ascending order of distance to their root.
+        /// </summary>
+        /// <returns>A collection of visuals.</returns>
+        public IEnumerator<IVisual> GetEnumerator()
+        {
+            foreach (var i in _inner)
+            {
+                foreach (var j in i.Value)
+                {
+                    yield return j;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the dirty visuals, in ascending order of distance to their root.
+        /// </summary>
+        /// <returns>A collection of visuals.</returns>
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}

+ 51 - 0
src/Avalonia.Visuals/Rendering/DisplayDirtyRect.cs

@@ -0,0 +1,51 @@
+using System;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// Holds the state for a dirty rect rendered when <see cref="IRenderer.DrawDirtyRects"/> is set.
+    /// </summary>
+    internal class DisplayDirtyRect
+    {
+        public static readonly TimeSpan TimeToLive = TimeSpan.FromMilliseconds(250);
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayDirtyRect"/> class.
+        /// </summary>
+        /// <param name="rect">The dirt rect.</param>
+        public DisplayDirtyRect(Rect rect)
+        {
+            Rect = rect;
+            ResetLifetime();
+        }
+
+        /// <summary>
+        /// Gets the bounds of the dirty rectangle.
+        /// </summary>
+        public Rect Rect { get; }
+
+        /// <summary>
+        /// Gets the time at which the rectangle was made dirty.
+        /// </summary>
+        public DateTimeOffset Born { get; private set; }
+
+        /// <summary>
+        /// Gets the time at which the rectagle should no longer be displayed.
+        /// </summary>
+        public DateTimeOffset Dies { get; private set; }
+
+        /// <summary>
+        /// Gets the opacity at which to display the dirty rectangle.
+        /// </summary>
+        public double Opacity => (Dies - DateTimeOffset.UtcNow).TotalMilliseconds / TimeToLive.TotalMilliseconds;
+
+        /// <summary>
+        /// Resets the rectangle's lifetime.
+        /// </summary>
+        public void ResetLifetime()
+        {
+            Born = DateTimeOffset.UtcNow;
+            Dies = Born + TimeToLive;
+        }
+    }
+}

+ 62 - 0
src/Avalonia.Visuals/Rendering/DisplayDirtyRects.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// Holds a collection of <see cref="DisplayDirtyRect"/> objects and manages their aging.
+    /// </summary>
+    internal class DisplayDirtyRects : IEnumerable<DisplayDirtyRect>
+    {
+        private List<DisplayDirtyRect> _inner = new List<DisplayDirtyRect>();
+
+        /// <summary>
+        /// Adds new new dirty rect to the collection.
+        /// </summary>
+        /// <param name="rect"></param>
+        public void Add(Rect rect)
+        {
+            foreach (var r in _inner)
+            {
+                if (r.Rect == rect)
+                {
+                    r.ResetLifetime();
+                    return;
+                }
+            }
+
+            _inner.Add(new DisplayDirtyRect(rect));
+        }
+
+        /// <summary>
+        /// Removes dirty rects one they are no longer active.
+        /// </summary>
+        public void Tick()
+        {
+            var now = DateTimeOffset.UtcNow;
+
+            for (var i = _inner.Count - 1; i >= 0; --i)
+            {
+                var r = _inner[i];
+
+                if (now > r.Dies)
+                {
+                    _inner.RemoveAt(i);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the dirty rects.
+        /// </summary>
+        /// <returns>A collection of <see cref="DisplayDirtyRect"/> objects.</returns>
+        public IEnumerator<DisplayDirtyRect> GetEnumerator() => _inner.GetEnumerator();
+
+        /// <summary>
+        /// Gets the dirty rects.
+        /// </summary>
+        /// <returns>A collection of <see cref="DisplayDirtyRect"/> objects.</returns>
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}

+ 11 - 0
src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs

@@ -0,0 +1,11 @@
+using System;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public interface IRenderLayerFactory
+    {
+        IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size, double dpiX, double dpiY);
+    }
+}

+ 5 - 0
src/Avalonia.Visuals/Rendering/IRenderRoot.cs

@@ -22,6 +22,11 @@ namespace Avalonia.Rendering
         /// </summary>
         IRenderer Renderer { get; }
 
+        /// <summary>
+        /// The scaling factor to use in rendering.
+        /// </summary>
+        double RenderScaling { get; }
+
         /// <summary>
         /// Creates a render target for the window.
         /// </summary>

+ 10 - 0
src/Avalonia.Visuals/Rendering/IRenderer.cs

@@ -48,5 +48,15 @@ namespace Avalonia.Rendering
         /// </summary>
         /// <param name="rect">The dirty rectangle.</param>
         void Paint(Rect rect);
+
+        /// <summary>
+        /// Starts the renderer.
+        /// </summary>
+        void Start();
+
+        /// <summary>
+        /// Stops the renderer.
+        /// </summary>
+        void Stop();
     }
 }

+ 0 - 18
src/Avalonia.Visuals/Rendering/IRendererFactory.cs

@@ -1,18 +0,0 @@
-using System;
-
-namespace Avalonia.Rendering
-{
-    /// <summary>
-    /// Defines a factory for creating <see cref="IRenderer"/> instances.
-    /// </summary>
-    public interface IRendererFactory
-    {
-        /// <summary>
-        /// Creates a new renderer for the specified render root.
-        /// </summary>
-        /// <param name="root">The render root.</param>
-        /// <param name="renderLoop">The render loop.</param>
-        /// <returns>An instance of an <see cref="IRenderer"/>.</returns>
-        IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop);
-    }
-}

+ 10 - 7
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -20,11 +20,6 @@ namespace Avalonia.Rendering
     /// </remarks>
     public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
     {
-        class ImmediateRendererFactory : IRendererFactory
-        {
-            public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) => new ImmediateRenderer(root);
-        }
-
         private readonly IVisual _root;
         private readonly IRenderRoot _renderRoot;
         private IRenderTarget _renderTarget;
@@ -41,8 +36,6 @@ namespace Avalonia.Rendering
             _renderRoot = root as IRenderRoot;
         }
 
-        public static IRendererFactory Factory { get; } = new ImmediateRendererFactory();
-
         /// <inheritdoc/>
         public bool DrawFps { get; set; }
 
@@ -148,6 +141,16 @@ namespace Avalonia.Rendering
             return HitTest(_root, p, filter);
         }
 
+        /// <inheritdoc/>
+        public void Start()
+        {
+        }
+
+        /// <inheritdoc/>
+        public void Stop()
+        {
+        }
+
         /// <inheritdoc/>
         Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
         {

+ 47 - 0
src/Avalonia.Visuals/Rendering/RenderLayer.cs

@@ -0,0 +1,47 @@
+using System;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class RenderLayer
+    {
+        private readonly IRenderLayerFactory _factory;
+
+        public RenderLayer(
+            IRenderLayerFactory factory,
+            Size size,
+            double scaling,
+            IVisual layerRoot)
+        {
+            _factory = factory;
+            Bitmap = factory.CreateLayer(layerRoot, size * scaling, 96 * scaling, 96 * scaling);
+            Size = size;
+            Scaling = scaling;
+            LayerRoot = layerRoot;
+        }
+
+        public IRenderTargetBitmapImpl Bitmap { get; private set; }
+        public double Scaling { get; private set; }
+        public Size Size { get; private set; }
+        public IVisual LayerRoot { get; }
+
+        public void ResizeBitmap(Size size, double scaling)
+        {
+            if (Size != size || Scaling != scaling)
+            {
+                var resized = _factory.CreateLayer(LayerRoot, size * scaling, 96 * scaling, 96 * scaling);
+
+                using (var context = resized.CreateDrawingContext(null))
+                {
+                    context.Clear(Colors.Transparent);
+                    context.DrawImage(Bitmap, 1, new Rect(Size), new Rect(Size));
+                    Bitmap.Dispose();
+                    Bitmap = resized;
+                    Size = size;
+                }
+            }
+        }
+    }
+}

+ 63 - 0
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class RenderLayers : IEnumerable<RenderLayer>
+    {
+        private readonly IRenderLayerFactory _factory;
+        private List<RenderLayer> _inner = new List<RenderLayer>();
+        private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
+
+        public RenderLayers(IRenderLayerFactory factory)
+        {
+            _factory = factory;
+        }
+
+        public int Count => _inner.Count;
+        public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
+
+        public void Update(Scene scene)
+        {
+            for (var i = scene.Layers.Count - 1; i >= 0; --i)
+            {
+                var src = scene.Layers[i];
+                RenderLayer layer;
+
+                if (!_index.TryGetValue(src.LayerRoot, out layer))
+                {
+                    layer = new RenderLayer(_factory, scene.Size, scene.Scaling, src.LayerRoot);
+                    _inner.Add(layer);
+                    _index.Add(src.LayerRoot, layer);
+                }
+                else
+                {
+                    layer.ResizeBitmap(scene.Size, scene.Scaling);
+                }
+            }
+
+            for (var i = _inner.Count - 1; i >= 0; --i)
+            {
+                var layer = _inner[i];
+
+                if (!scene.Layers.Exists(layer.LayerRoot))
+                {
+                    layer.Bitmap.Dispose();
+                    _inner.RemoveAt(i);
+                    _index.Remove(layer.LayerRoot);
+                }
+            }
+        }
+
+        public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
+        {
+            return _index.TryGetValue(layerRoot, out value);
+        }
+
+        public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}

+ 31 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@@ -0,0 +1,31 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Base class for draw operations that can use a brush.
+    /// </summary>
+    internal abstract class BrushDrawOperation : IDrawOperation
+    {
+        /// <inheritdoc/>
+        public abstract Rect Bounds { get; }
+
+        /// <inheritdoc/>
+        public abstract bool HitTest(Point p);
+
+        /// <summary>
+        /// Gets a collection of child scenes that are needed to draw visual brushes.
+        /// </summary>
+        public abstract IDictionary<IVisual, Scene> ChildScenes { get; }
+
+        /// <inheritdoc/>
+        public abstract void Render(IDrawingContextImpl context);
+    }
+}

+ 64 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs

@@ -0,0 +1,64 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents a clip push or pop.
+    /// </summary>
+    internal class ClipNode : IDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
+        /// clip push.
+        /// </summary>
+        /// <param name="clip">The clip to push.</param>
+        public ClipNode(Rect clip)
+        {
+            Clip = clip;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
+        /// clip pop.
+        /// </summary>
+        public ClipNode()
+        {
+        }
+
+        /// <inheritdoc/>
+        public Rect Bounds => Rect.Empty;
+
+        /// <summary>
+        /// Gets the clip to be pushed or null if the operation represents a pop.
+        /// </summary>
+        public Rect? Clip { get; }
+
+        /// <inheritdoc/>
+        public bool HitTest(Point p) => false;
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="clip">The clip of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        public bool Equals(Rect? clip) => Clip == clip;
+
+        /// <inheritdoc/>
+        public void Render(IDrawingContextImpl context)
+        {
+            if (Clip.HasValue)
+            {
+                context.PushClip(Clip.Value);
+            }
+            else
+            {
+                context.PopClip();
+            }
+        }
+    }
+}

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

@@ -0,0 +1,392 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A drawing context which builds a scene graph.
+    /// </summary>
+    internal class DeferredDrawingContextImpl : IDrawingContextImpl
+    {
+        private readonly ISceneBuilder _sceneBuilder;
+        private VisualNode _node;
+        private int _childIndex;
+        private int _drawOperationindex;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DeferredDrawingContextImpl"/> class.
+        /// </summary>
+        /// <param name="sceneBuilder">
+        /// A scene builder used for constructing child scenes for visual brushes.
+        /// </param>
+        /// <param name="layers">The scene layers.</param>
+        public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers)
+        {
+            _sceneBuilder = sceneBuilder;
+            Layers = layers;
+        }
+
+        /// <inheritdoc/>
+        public Matrix Transform { get; set; } = Matrix.Identity;
+
+        /// <summary>
+        /// Gets the layers in the scene being built.
+        /// </summary>
+        public SceneLayers Layers { get; }
+
+        /// <summary>
+        /// Informs the drawing context of the visual node that is about to be rendered.
+        /// </summary>
+        /// <param name="node">The visual node.</param>
+        /// <returns>
+        /// An object which when disposed will commit the changes to visual node.
+        /// </returns>
+        public UpdateState BeginUpdate(VisualNode node)
+        {
+            Contract.Requires<ArgumentNullException>(node != null);
+
+            if (_node != null)
+            {
+                if (_childIndex < _node.Children.Count)
+                {
+                    _node.ReplaceChild(_childIndex, node);
+                }
+                else
+                {
+                    _node.AddChild(node);
+                }
+
+                ++_childIndex;
+            }
+
+            var state = new UpdateState(this, _node, _childIndex, _drawOperationindex);
+            _node = node;
+            _childIndex = _drawOperationindex = 0;
+            return state;
+        }
+
+        /// <inheritdoc/>
+        public void Clear(Color color)
+        {
+            // Cannot clear a deferred scene.
+        }
+
+        /// <inheritdoc/>
+        public void Dispose()
+        {
+            // Nothing to do here as we allocate no unmanaged resources.
+        }
+
+        /// <summary>
+        /// Removes any remaining drawing operations from the visual node.
+        /// </summary>
+        /// <remarks>
+        /// Drawing operations are updated in place, overwriting existing drawing operations if
+        /// they are different. Once drawing has completed for the current visual node, it is
+        /// possible that there are stale drawing operations at the end of the list. This method
+        /// trims these stale drawing operations.
+        /// </remarks>
+        public void TrimChildren()
+        {
+            _node.TrimChildren(_childIndex);
+        }
+
+        /// <inheritdoc/>
+        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+        {
+            var next = NextDrawAs<GeometryNode>();
+
+            if (next == null || !next.Equals(Transform, brush, pen, geometry))
+            {
+                Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
+        {
+            var next = NextDrawAs<ImageNode>();
+
+            if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect))
+            {
+                Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
+        {
+            // This method is currently only used to composite layers so shouldn't be called here.
+            throw new NotSupportedException();
+        }
+
+        /// <inheritdoc/>
+        public void DrawLine(Pen pen, Point p1, Point p2)
+        {
+            var next = NextDrawAs<LineNode>();
+
+            if (next == null || !next.Equals(Transform, pen, p1, p2))
+            {
+                Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+        {
+            var next = NextDrawAs<RectangleNode>();
+
+            if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius))
+            {
+                Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush)));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+        {
+            var next = NextDrawAs<TextNode>();
+
+            if (next == null || !next.Equals(Transform, foreground, origin, text))
+            {
+                Add(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground)));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+        {
+            var next = NextDrawAs<RectangleNode>();
+
+            if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius))
+            {
+                Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush)));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PopClip()
+        {
+            var next = NextDrawAs<ClipNode>();
+
+            if (next == null || !next.Equals(null))
+            {
+                Add(new ClipNode());
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PopGeometryClip()
+        {
+            var next = NextDrawAs<GeometryClipNode>();
+
+            if (next == null || !next.Equals(null))
+            {
+                Add(new GeometryClipNode());
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PopOpacity()
+        {
+            var next = NextDrawAs<OpacityNode>();
+
+            if (next == null || !next.Equals(null))
+            {
+                Add(new OpacityNode());
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PopOpacityMask()
+        {
+            var next = NextDrawAs<OpacityMaskNode>();
+
+            if (next == null || !next.Equals(null, null))
+            {
+                Add(new OpacityMaskNode());
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PushClip(Rect clip)
+        {
+            var next = NextDrawAs<ClipNode>();
+
+            if (next == null || !next.Equals(clip))
+            {
+                Add(new ClipNode(clip));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PushGeometryClip(IGeometryImpl clip)
+        {
+            var next = NextDrawAs<GeometryClipNode>();
+
+            if (next == null || !next.Equals(clip))
+            {
+                Add(new GeometryClipNode(clip));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PushOpacity(double opacity)
+        {
+            var next = NextDrawAs<OpacityNode>();
+
+            if (next == null || !next.Equals(opacity))
+            {
+                Add(new OpacityNode(opacity));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void PushOpacityMask(IBrush mask, Rect bounds)
+        {
+            var next = NextDrawAs<OpacityMaskNode>();
+
+            if (next == null || !next.Equals(mask, bounds))
+            {
+                Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
+            }
+            else
+            {
+                ++_drawOperationindex;
+            }
+        }
+
+        public struct UpdateState : IDisposable
+        {
+            public UpdateState(
+                DeferredDrawingContextImpl owner,
+                VisualNode node,
+                int childIndex,
+                int drawOperationIndex)
+            {
+                Owner = owner;
+                Node = node;
+                ChildIndex = childIndex;
+                DrawOperationIndex = drawOperationIndex;
+            }
+
+            public void Dispose()
+            {
+                Owner._node.TrimDrawOperations(Owner._drawOperationindex);
+
+                var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot).Dirty;
+
+                foreach (var operation in Owner._node.DrawOperations)
+                {
+                    dirty.Add(operation.Bounds);
+                }
+
+                Owner._node = Node;
+                Owner._childIndex = ChildIndex;
+                Owner._drawOperationindex = DrawOperationIndex;
+            }
+
+            public DeferredDrawingContextImpl Owner { get; }
+            public VisualNode Node { get; }
+            public int ChildIndex { get; }
+            public int DrawOperationIndex { get; }
+        }
+
+        private void Add(IDrawOperation  node)
+        {
+            if (_drawOperationindex < _node.DrawOperations.Count)
+            {
+                _node.ReplaceDrawOperation(_drawOperationindex, node);
+            }
+            else
+            {
+                _node.AddDrawOperation(node);
+            }
+
+            ++_drawOperationindex;
+        }
+
+        private T NextDrawAs<T>() where T : class, IDrawOperation
+        {
+            return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as T : null;
+        }
+
+        private IDictionary<IVisual, Scene> CreateChildScene(IBrush brush)
+        {
+            var visualBrush = brush as VisualBrush;
+
+            if (visualBrush != null)
+            {
+                var visual = visualBrush.Visual;
+
+                if (visual != null)
+                {
+                    (visual as IVisualBrushInitialize)?.EnsureInitialized();
+                    var scene = new Scene(visual);
+                    _sceneBuilder.UpdateAll(scene);
+                    return new Dictionary<IVisual, Scene> { { visualBrush.Visual, scene } };
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 64 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs

@@ -0,0 +1,64 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents a geometry clip push or pop.
+    /// </summary>
+    internal class GeometryClipNode : IDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
+        /// geometry clip push.
+        /// </summary>
+        /// <param name="clip">The clip to push.</param>
+        public GeometryClipNode(IGeometryImpl clip)
+        {
+            Clip = clip;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
+        /// geometry clip pop.
+        /// </summary>
+        public GeometryClipNode()
+        {
+        }
+
+        /// <inheritdoc/>
+        public Rect Bounds => Rect.Empty;
+
+        /// <summary>
+        /// Gets the clip to be pushed or null if the operation represents a pop.
+        /// </summary>
+        public IGeometryImpl Clip { get; }
+
+        /// <inheritdoc/>
+        public bool HitTest(Point p) => false;
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="clip">The clip of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        public bool Equals(IGeometryImpl clip) => Clip == clip;
+
+        /// <inheritdoc/>
+        public void Render(IDrawingContextImpl context)
+        {
+            if (Clip != null)
+            {
+                context.PushGeometryClip(Clip);
+            }
+            else
+            {
+                context.PopGeometryClip();
+            }
+        }
+    }
+}

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

@@ -0,0 +1,101 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents a geometry draw.
+    /// </summary>
+    internal class GeometryNode : BrushDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GeometryNode"/> class.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <param name="brush">The fill brush.</param>
+        /// <param name="pen">The stroke pen.</param>
+        /// <param name="geometry">The geometry.</param>
+        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        public GeometryNode(
+            Matrix transform,
+            IBrush brush,
+            Pen pen,
+            IGeometryImpl geometry,
+            IDictionary<IVisual, Scene> childScenes = null)
+        {
+            Bounds = geometry.GetRenderBounds(pen?.Thickness ?? 0).TransformToAABB(transform);
+            Transform = transform;
+            Brush = brush?.ToImmutable();
+            Pen = pen?.ToImmutable();
+            Geometry = geometry;
+            ChildScenes = childScenes;
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds { get; }
+
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
+        /// <summary>
+        /// Gets the fill brush.
+        /// </summary>
+        public IBrush Brush { get; }
+
+        /// <summary>
+        /// Gets the stroke pen.
+        /// </summary>
+        public Pen Pen { get; }
+
+        /// <summary>
+        /// Gets the geometry to draw.
+        /// </summary>
+        public IGeometryImpl Geometry { get; }
+
+        /// <inheritdoc/>
+        public override IDictionary<IVisual, Scene> ChildScenes { get; }
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
+        /// <param name="brush">The fill of the other draw operation.</param>
+        /// <param name="pen">The stroke of the other draw operation.</param>
+        /// <param name="geometry">The geometry of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
+        {
+            return transform == Transform &&
+                Equals(brush, Brush) && 
+                pen == Pen &&
+                Equals(geometry, Geometry);
+        }
+
+        /// <inheritdoc/>
+        public override void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawGeometry(Brush, Pen, Geometry);
+        }
+
+        /// <inheritdoc/>
+        public override bool HitTest(Point p)
+        {
+            p *= Transform.Invert();
+            return (Brush != null && Geometry.FillContains(p)) || 
+                (Pen != null && Geometry.StrokeContains(Pen, p));
+        }
+    }
+}

+ 36 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs

@@ -0,0 +1,36 @@
+// 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.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Represents a node in the low-level scene graph that represents geometry.
+    /// </summary>
+    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>
+        /// <param name="p">The point in global coordinates.</param>
+        /// <returns>True if the point hits the node's geometry; otherwise false.</returns>
+        /// <remarks>
+        /// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
+        /// 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);
+    }
+}

+ 24 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs

@@ -0,0 +1,24 @@
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Builds a scene graph from a visual tree.
+    /// </summary>
+    public interface ISceneBuilder
+    {
+        /// <summary>
+        /// Builds the initial scene graph for a visual tree.
+        /// </summary>
+        /// <param name="scene">The scene to build.</param>
+        void UpdateAll(Scene scene);
+
+        /// <summary>
+        /// Updates the visual (and potentially its children) in a scene.
+        /// </summary>
+        /// <param name="scene">The scene.</param>
+        /// <param name="visual">The visual to update.</param>
+        /// <returns>True if changes were made, otherwise false.</returns>
+        bool Update(Scene scene, IVisual visual);
+    }
+}

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

@@ -0,0 +1,94 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Represents a node in the low-level scene graph representing an <see cref="IVisual"/>.
+    /// </summary>
+    public interface IVisualNode
+    {
+        /// <summary>
+        /// Gets the visual to which the node relates.
+        /// </summary>
+        IVisual Visual { get; }
+
+        /// <summary>
+        /// Gets the parent scene graph node.
+        /// </summary>
+        IVisualNode Parent { get; }
+
+        /// <summary>
+        /// Gets the transform for the node from global to control coordinates.
+        /// </summary>
+        Matrix Transform { get; }
+
+        /// <summary>
+        /// Gets the bounds for the node's geometry in global coordinates.
+        /// </summary>
+        Rect Bounds { get; }
+
+        /// <summary>
+        /// Gets the clip bounds for the node in global coordinates.
+        /// </summary>
+        /// <remarks>
+        /// This clip does not take into account parent clips, to find the absolute clip bounds
+        /// it is necessary to traverse the tree.
+        /// </remarks>
+        Rect ClipBounds { get; }
+
+        /// <summary>
+        /// Whether the node is clipped to <see cref="ClipBounds"/>.
+        /// </summary>
+        bool ClipToBounds { get; }
+
+        /// <summary>
+        /// Gets the node's clip geometry, if any.
+        /// </summary>
+        IGeometryImpl GeometryClip { get; set; }
+
+        /// <summary>
+        /// Gets a value indicating whether one of the node's ancestors has a geometry clip.
+        /// </summary>
+        bool HasAncestorGeometryClip { get; }
+
+        /// <summary>
+        /// Gets the child scene graph nodes.
+        /// </summary>
+        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.
+        /// </summary>
+        /// <param name="p">The point in global coordinates.</param>
+        /// <returns>True if the point hits the node's geometry; otherwise false.</returns>
+        /// <remarks>
+        /// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
+        /// to hit test children they must be hit tested manually.
+        /// </remarks>
+        bool HitTest(Point p);
+    }
+}

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

@@ -0,0 +1,94 @@
+// 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.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents an image draw.
+    /// </summary>
+    internal class ImageNode : IDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImageNode"/> class.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <param name="source">The image to draw.</param>
+        /// <param name="opacity">The draw opacity.</param>
+        /// <param name="sourceRect">The source rect.</param>
+        /// <param name="destRect">The destination rect.</param>
+        public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
+        {
+            Bounds = destRect.TransformToAABB(transform);
+            Transform = transform;
+            Source = source;
+            Opacity = opacity;
+            SourceRect = sourceRect;
+            DestRect = destRect;
+        }
+
+        /// <inheritdoc/>
+        public Rect Bounds { get; }
+
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
+        /// <summary>
+        /// Gets the image to draw.
+        /// </summary>
+        public IBitmapImpl Source { get; }
+
+        /// <summary>
+        /// Gets the draw opacity.
+        /// </summary>
+        public double Opacity { get; }
+
+        /// <summary>
+        /// Gets the source rect.
+        /// </summary>
+        public Rect SourceRect { get; }
+
+        /// <summary>
+        /// Gets the destination rect.
+        /// </summary>
+        public Rect DestRect { get; }
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
+        /// <param name="source">The image of the other draw operation.</param>
+        /// <param name="opacity">The opacity of the other draw operation.</param>
+        /// <param name="sourceRect">The source rect of the other draw operation.</param>
+        /// <param name="destRect">The dest rect of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        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;
+        }
+
+        /// <inheritdoc/>
+        public void Render(IDrawingContextImpl context)
+        {
+            // TODO: Probably need to introduce some kind of locking mechanism in the case of
+            // WriteableBitmap.
+            context.Transform = Transform;
+            context.DrawImage(Source, Opacity, SourceRect, DestRect);
+        }
+
+        /// <inheritdoc/>
+        public bool HitTest(Point p) => Bounds.Contains(p);
+    }
+}

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

@@ -0,0 +1,95 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents a line draw.
+    /// </summary>
+    internal class LineNode : BrushDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GeometryNode"/> class.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <param name="pen">The stroke pen.</param>
+        /// <param name="p1">The start point of the line.</param>
+        /// <param name="p2">The end point of the line.</param>
+        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        public LineNode(
+            Matrix transform,
+            Pen pen,
+            Point p1,
+            Point p2,
+            IDictionary<IVisual, Scene> childScenes = null)
+        {
+            Bounds = new Rect(P1, P2);
+            Transform = transform;
+            Pen = pen?.ToImmutable();
+            P1 = p1;
+            P2 = p2;
+            ChildScenes = childScenes;
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds { get; }
+
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
+        /// <summary>
+        /// Gets the stroke pen.
+        /// </summary>
+        public Pen Pen { get; }
+
+        /// <summary>
+        /// Gets the start point of the line.
+        /// </summary>
+        public Point P1 { get; }
+
+        /// <summary>
+        /// Gets the end point of the line.
+        /// </summary>
+        public Point P2 { get; }
+
+        /// <inheritdoc/>
+        public override IDictionary<IVisual, Scene> ChildScenes { get; }
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
+        /// <param name="pen">The stroke of the other draw operation.</param>
+        /// <param name="p1">The start point of the other draw operation.</param>
+        /// <param name="p2">The end point of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        public bool Equals(Matrix transform, Pen pen, Point p1, Point p2)
+        {
+            return transform == Transform && pen == Pen && p1 == P1 && p2 == P2;
+        }
+
+        public override void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawLine(Pen, P1, P2);
+        }
+
+        public override bool HitTest(Point p)
+        {
+            // TODO: Implement line hit testing.
+            return false;
+        }
+    }
+}

+ 80 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents an opacity mask push or pop.
+    /// </summary>
+    internal class OpacityMaskNode : BrushDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
+        /// opacity mask push.
+        /// </summary>
+        /// <param name="mask">The opacity mask to push.</param>
+        /// <param name="bounds">The bounds of the mask.</param>
+        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary<IVisual, Scene> childScenes = null)
+        {
+            Mask = mask?.ToImmutable();
+            MaskBounds = bounds;
+            ChildScenes = childScenes;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
+        /// opacity mask pop.
+        /// </summary>
+        public OpacityMaskNode()
+        {
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds => Rect.Empty;
+
+        /// <summary>
+        /// Gets the mask to be pushed or null if the operation represents a pop.
+        /// </summary>
+        public IBrush Mask { get; }
+
+        /// <summary>
+        /// Gets the bounds of the opacity mask or null if the operation represents a pop.
+        /// </summary>
+        public Rect? MaskBounds { get; }
+
+        /// <inheritdoc/>
+        public override IDictionary<IVisual, Scene> ChildScenes { get; }
+
+        /// <inheritdoc/>
+        public override bool HitTest(Point p) => false;
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="mask">The opacity mask of the other draw operation.</param>
+        /// <param name="bounds">The opacity mask bounds of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        public bool Equals(IBrush mask, Rect? bounds) => Mask == mask && MaskBounds == bounds;
+
+        /// <inheritdoc/>
+        public override void Render(IDrawingContextImpl context)
+        {
+            if (Mask != null)
+            {
+                context.PushOpacityMask(Mask, MaskBounds.Value);
+            }
+            else
+            {
+                context.PopOpacityMask();
+            }
+        }
+    }
+}

+ 64 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs

@@ -0,0 +1,64 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents an opacity push or pop.
+    /// </summary>
+    internal class OpacityNode : IDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OpacityNode"/> class that represents an
+        /// opacity push.
+        /// </summary>
+        /// <param name="opacity">The opacity to push.</param>
+        public OpacityNode(double opacity)
+        {
+            Opacity = opacity;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OpacityNode"/> class that represents an
+        /// opacity pop.
+        /// </summary>
+        public OpacityNode()
+        {
+        }
+
+        /// <inheritdoc/>
+        public Rect Bounds => Rect.Empty;
+
+        /// <summary>
+        /// Gets the opacity to be pushed or null if the operation represents a pop.
+        /// </summary>
+        public double? Opacity { get; }
+
+        /// <inheritdoc/>
+        public bool HitTest(Point p) => false;
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="opacity">The opacity of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        public bool Equals(double? opacity) => Opacity == opacity;
+
+        /// <inheritdoc/>
+        public void Render(IDrawingContextImpl context)
+        {
+            if (Opacity.HasValue)
+            {
+                context.PushOpacity(Opacity.Value);
+            }
+            else
+            {
+                context.PopOpacity();
+            }
+        }
+    }
+}

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

@@ -0,0 +1,116 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents a rectangle draw.
+    /// </summary>
+    internal class RectangleNode : BrushDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RectangleNode"/> class.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <param name="brush">The fill brush.</param>
+        /// <param name="pen">The stroke pen.</param>
+        /// <param name="rect">The rectanle to draw.</param>
+        /// <param name="cornerRadius">The rectangle corner radius.</param>
+        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        public RectangleNode(
+            Matrix transform,
+            IBrush brush,
+            Pen pen,
+            Rect rect,
+            float cornerRadius,
+            IDictionary<IVisual, Scene> childScenes = null)
+        {
+            Bounds = rect.TransformToAABB(transform).Inflate(pen?.Thickness ?? 0);
+            Transform = transform;
+            Brush = brush?.ToImmutable();
+            Pen = pen?.ToImmutable();
+            Rect = rect;
+            CornerRadius = cornerRadius;
+            ChildScenes = childScenes;
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds { get; }
+
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
+        /// <summary>
+        /// Gets the fill brush.
+        /// </summary>
+        public IBrush Brush { get; }
+
+        /// <summary>
+        /// Gets the stroke pen.
+        /// </summary>
+        public Pen Pen { get; }
+
+        /// <summary>
+        /// Gets the rectangle to draw.
+        /// </summary>
+        public Rect Rect { get; }
+
+        /// <summary>
+        /// Gets the rectangle corner radius.
+        /// </summary>
+        public float CornerRadius { get; }
+
+        /// <inheritdoc/>
+        public override IDictionary<IVisual, Scene> ChildScenes { get; }
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
+        /// <param name="brush">The fill of the other draw operation.</param>
+        /// <param name="pen">The stroke of the other draw operation.</param>
+        /// <param name="rect">The rectangle of the other draw operation.</param>
+        /// <param name="cornerRadius">The rectangle corner radius of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        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;
+        }
+
+        /// <inheritdoc/>
+        public override void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+
+            if (Brush != null)
+            {
+                context.FillRectangle(Brush, Rect, CornerRadius);
+            }
+
+            if (Pen != null)
+            {
+                context.DrawRectangle(Pen, Rect, CornerRadius);
+            }
+        }
+
+        // TODO: This doesn't respect CornerRadius yet.
+        /// <inheritdoc/>
+        public override bool HitTest(Point p) => Bounds.Contains(p);
+    }
+}

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

@@ -0,0 +1,184 @@
+// 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
+{
+    /// <summary>
+    /// Represents a scene graph used by the <see cref="DeferredRenderer"/>.
+    /// </summary>
+    public class Scene
+    {
+        private Dictionary<IVisual, IVisualNode> _index;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Scene"/> class.
+        /// </summary>
+        /// <param name="rootVisual">The root visual to draw.</param>
+        public Scene(IVisual rootVisual)
+            : this(
+                new VisualNode(rootVisual, null),
+                new Dictionary<IVisual, IVisualNode>(),
+                new SceneLayers(rootVisual),
+                0)
+        {
+            _index.Add(rootVisual, Root);
+        }
+
+        private Scene(VisualNode root, Dictionary<IVisual, IVisualNode> index, SceneLayers layers, int generation)
+        {
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            var renderRoot = root.Visual as IRenderRoot;
+
+            _index = index;
+            Root = root;
+            Layers = layers;
+            Generation = generation;
+            root.LayerRoot = root.Visual;
+        }
+
+        /// <summary>
+        /// Gets a value identifying the scene's generation. This is incremented each time the scene is cloned.
+        /// </summary>
+        public int Generation { get; }
+
+        /// <summary>
+        /// Gets the layers for the scene.
+        /// </summary>
+        public SceneLayers Layers { get; }
+
+        /// <summary>
+        /// Gets the root node of the scene graph.
+        /// </summary>
+        public IVisualNode Root { get; }
+
+        /// <summary>
+        /// Gets or sets the size of the scene in device independent pixels.
+        /// </summary>
+        public Size Size { get; set; }
+
+        /// <summary>
+        /// Gets or sets the scene scaling.
+        /// </summary>
+        public double Scaling { get; set; } = 1;
+
+        /// <summary>
+        /// Adds a node to the scene index.
+        /// </summary>
+        /// <param name="node">The node.</param>
+        public void Add(IVisualNode node)
+        {
+            Contract.Requires<ArgumentNullException>(node != null);
+
+            _index.Add(node.Visual, node);
+        }
+
+        /// <summary>
+        /// Clones the scene.
+        /// </summary>
+        /// <returns>The cloned scene.</returns>
+        public Scene Clone()
+        {
+            var index = new Dictionary<IVisual, IVisualNode>();
+            var root = Clone((VisualNode)Root, null, index);
+
+            var result = new Scene(root, index, Layers.Clone(), Generation + 1)
+            {
+                Size = Size,
+                Scaling = Scaling,
+            };
+
+            return result;
+        }
+
+        /// <summary>
+        /// Tries to find a node in the scene graph representing the specified visual.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <returns>
+        /// The node representing the visual or null if it could not be found.
+        /// </returns>
+        public IVisualNode FindNode(IVisual visual)
+        {
+            IVisualNode node;
+            _index.TryGetValue(visual, out node);
+            return node;
+        }
+
+        /// <summary>
+        /// Gets the visuals at a point in the scene.
+        /// </summary>
+        /// <param name="p">The point.</param>
+        /// <param name="filter">A filter. May be null.</param>
+        /// <returns>The visuals at the specified point.</returns>
+        public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
+        {
+            return HitTest(Root, p, null, filter);
+        }
+
+        /// <summary>
+        /// Removes a node from the scene index.
+        /// </summary>
+        /// <param name="node">The node.</param>
+        public void Remove(IVisualNode node)
+        {
+            Contract.Requires<ArgumentNullException>(node != null);
+
+            _index.Remove(node.Visual);
+        }
+
+        private VisualNode Clone(VisualNode source, IVisualNode parent, Dictionary<IVisual, IVisualNode> index)
+        {
+            var result = source.Clone(parent);
+
+            index.Add(result.Visual, result);
+
+            foreach (var child in source.Children)
+            {
+                result.AddChild(Clone((VisualNode)child, result, index));
+            }
+
+            return result;
+        }
+
+        private IEnumerable<IVisual> HitTest(IVisualNode node, Point p, Rect? clip, Func<IVisual, bool> filter)
+        {
+            if (filter?.Invoke(node.Visual) != false)
+            {
+                var clipped = false;
+
+                if (node.ClipToBounds)
+                {
+                    clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
+                    clipped = !clip.Value.Contains(p);
+                }
+
+                if (node.GeometryClip != null)
+                {
+                    var controlPoint = Root.Visual.TranslatePoint(p, node.Visual);
+                    clipped = !node.GeometryClip.FillContains(controlPoint);
+                }
+
+                if (!clipped)
+                {
+                    for (var i = node.Children.Count - 1; i >= 0; --i)
+                    {
+                        foreach (var h in HitTest(node.Children[i], p, clip, filter))
+                        {
+                            yield return h;
+                        }
+                    }
+
+                    if (node.HitTest(p))
+                    {
+                        yield return node.Visual;
+                    }
+                }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,384 @@
+// 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.Linq;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Builds a scene graph from a visual tree.
+    /// </summary>
+    public class SceneBuilder : ISceneBuilder
+    {
+        /// <inheritdoc/>
+        public void UpdateAll(Scene scene)
+        {
+            Contract.Requires<ArgumentNullException>(scene != null);
+            Dispatcher.UIThread.VerifyAccess();
+
+            UpdateSize(scene);
+            scene.Layers.GetOrAdd(scene.Root.Visual);
+
+            using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
+            using (var context = new DrawingContext(impl))
+            {
+                Update(context, scene, (VisualNode)scene.Root, scene.Root.Visual.Bounds, true);
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool Update(Scene scene, IVisual visual)
+        {
+            Contract.Requires<ArgumentNullException>(scene != null);
+            Contract.Requires<ArgumentNullException>(visual != null);
+            Dispatcher.UIThread.VerifyAccess();
+
+            var node = (VisualNode)scene.FindNode(visual);
+
+            if (visual == scene.Root.Visual)
+            {
+                UpdateSize(scene);
+            }
+
+            if (visual.VisualRoot != null)
+            {
+                if (visual.IsVisible)
+                {
+                    // If the node isn't yet part of the scene, find the nearest ancestor that is.
+                    node = node ?? FindExistingAncestor(scene, visual);
+
+                    // We don't need to do anything if this part of the tree has already been fully
+                    // updated.
+                    if (node != null && !node.SubTreeUpdated)
+                    {
+                        // If the control we've been asked to update isn't part of the scene then
+                        // we're carrying out an add operation, so recurse and add all the
+                        // descendents too.
+                        var recurse = node.Visual != visual;
+
+                        using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
+                        using (var context = new DrawingContext(impl))
+                        {
+                            var clip = scene.Root.Visual.Bounds;
+
+                            if (node.Parent != null)
+                            {
+                                context.PushPostTransform(node.Parent.Transform);
+                                clip = node.Parent.ClipBounds;
+                            }
+
+                            using (context.PushTransformContainer())
+                            {
+                                Update(context, scene, node, clip, recurse);
+                            }
+                        }
+
+                        return true;
+                    }
+                }
+                else
+                {
+                    if (node != null)
+                    {
+                        // The control has been hidden so remove it from its parent and deindex the
+                        // node and its descendents.
+                        ((VisualNode)node.Parent)?.RemoveChild(node);
+                        Deindex(scene, node);
+                        return true;
+                    }
+                }
+            }
+            else if (node != null)
+            {
+                // The control has been removed so remove it from its parent and deindex the
+                // node and its descendents.
+                var trim = FindFirstDeadAncestor(scene, node);
+                ((VisualNode)trim.Parent).RemoveChild(trim);
+                Deindex(scene, trim);
+                return true;
+            }
+
+            return false;
+        }
+
+        private static VisualNode FindExistingAncestor(Scene scene, IVisual visual)
+        {
+            var node = scene.FindNode(visual);
+
+            while (node == null && visual.IsVisible)
+            {
+                visual = visual.VisualParent;
+                node = scene.FindNode(visual);
+            }
+
+            return visual.IsVisible ? (VisualNode)node : null;
+        }
+
+        private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node)
+        {
+            var parent = node.Parent;
+
+            while (parent.Visual.VisualRoot == null)
+            {
+                node = parent;
+                parent = node.Parent;
+            }
+
+            return (VisualNode)node;
+        }
+
+        private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse)
+        {
+            var visual = node.Visual;
+            var opacity = visual.Opacity;
+            var clipToBounds = visual.ClipToBounds;
+            var bounds = new Rect(visual.Bounds.Size);
+            var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
+
+            contextImpl.Layers.Find(node.LayerRoot)?.Dirty.Add(node.Bounds);
+
+            if (visual.IsVisible)
+            {
+                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.BeginUpdate(node))
+                using (context.PushPostTransform(m))
+                using (context.PushTransformContainer())
+                {
+                    var startLayer = opacity < 1 || visual.OpacityMask != null;
+
+                    forceRecurse = forceRecurse || node.Transform != contextImpl.Transform;
+
+                    node.Transform = contextImpl.Transform;
+                    node.ClipBounds = bounds.TransformToAABB(node.Transform).Intersect(clip);
+                    node.ClipToBounds = clipToBounds;
+                    node.GeometryClip = visual.Clip?.PlatformImpl;
+                    node.Opacity = opacity;
+                    node.OpacityMask = visual.OpacityMask;
+
+                    if (startLayer)
+                    {
+                        if (node.LayerRoot != visual)
+                        {
+                            MakeLayer(scene, node);
+                        }
+                        else
+                        {
+                            UpdateLayer(node, scene.Layers[node.LayerRoot]);
+                        }
+                    }
+                    else if (!startLayer && node.LayerRoot == node.Visual && node.Parent != null)
+                    {
+                        ClearLayer(scene, node);
+                    }
+
+                    if (node.ClipToBounds)
+                    {
+                        clip = clip.Intersect(node.ClipBounds);
+                    }
+
+                    try
+                    {
+                        visual.Render(context);
+                    }
+                    catch { }
+
+                    if (visual is Visual)
+                    {
+                        var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform);
+                        BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
+                    }
+
+                    if (forceRecurse)
+                    {
+                        foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
+                        {
+                            var childNode = scene.FindNode(child) ?? CreateNode(scene, child, node);
+                            Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
+                        }
+
+                        node.SubTreeUpdated = true;
+                        contextImpl.TrimChildren();
+                    }
+                }
+            }
+        }
+
+        private void UpdateSize(Scene scene)
+        {
+            var renderRoot = scene.Root.Visual as IRenderRoot;
+            var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size;
+
+            scene.Scaling = renderRoot?.RenderScaling ?? 1;
+
+            if (scene.Size != newSize)
+            {
+                var oldSize = scene.Size;
+
+                scene.Size = newSize;
+
+                Rect horizontalDirtyRect = Rect.Empty;
+                Rect verticalDirtyRect = Rect.Empty;
+
+                if (newSize.Width > oldSize.Width)
+                {
+                    horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height);
+                }
+
+                if (newSize.Height > oldSize.Height)
+                {
+                    verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height);
+                }
+
+                foreach (var layer in scene.Layers)
+                {
+                    layer.Dirty.Add(horizontalDirtyRect);
+                    layer.Dirty.Add(verticalDirtyRect);
+                }
+            }
+        }
+
+        private static VisualNode CreateNode(Scene scene, IVisual visual, VisualNode parent)
+        {
+            var node = new VisualNode(visual, parent);
+            node.LayerRoot = parent.LayerRoot;
+            scene.Add(node);
+            return node;
+        }
+
+        private static void Deindex(Scene scene, VisualNode node)
+        {
+            scene.Remove(node);
+            node.SubTreeUpdated = true;
+
+            scene.Layers[node.LayerRoot].Dirty.Add(node.Bounds);
+
+            if (node.Visual is Visual v)
+            {
+                BoundsTracker.SetTransformedBounds(v, null);
+            }
+
+            foreach (VisualNode child in node.Children)
+            {
+                var geometry = child as IDrawOperation;
+
+                if (child is VisualNode visual)
+                {
+                    Deindex(scene, visual);
+                }
+            }
+
+            if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual)
+            {
+                scene.Layers.Remove(node.LayerRoot);
+            }
+        }
+
+        private static void ClearLayer(Scene scene, VisualNode node)
+        {
+            var parent = (VisualNode)node.Parent;
+            var oldLayerRoot = node.LayerRoot;
+            var newLayerRoot = parent.LayerRoot;
+            var existingDirtyRects = scene.Layers[node.LayerRoot].Dirty;
+            var newDirtyRects = scene.Layers[newLayerRoot].Dirty;
+
+            existingDirtyRects.Coalesce();
+
+            foreach (var r in existingDirtyRects)
+            {
+                newDirtyRects.Add(r);
+            }
+
+            var oldLayer = scene.Layers[oldLayerRoot];
+            PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer);
+            scene.Layers.Remove(oldLayer);
+        }
+
+        private static void MakeLayer(Scene scene, VisualNode node)
+        {
+            var oldLayerRoot = node.LayerRoot;
+            var layer = scene.Layers.Add(node.Visual);
+            var oldLayer = scene.Layers[oldLayerRoot];
+
+            UpdateLayer(node, layer);
+            PropagateLayer(node, layer, scene.Layers[oldLayerRoot]);
+        }
+
+        private static void UpdateLayer(VisualNode node, SceneLayer layer)
+        {
+            layer.Opacity = node.Visual.Opacity;
+
+            if (node.Visual.OpacityMask != null)
+            {
+                layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable();
+                layer.OpacityMaskRect = node.ClipBounds;
+            }
+            else
+            {
+                layer.OpacityMask = null;
+                layer.OpacityMaskRect = Rect.Empty;
+            }
+
+            layer.GeometryClip = node.HasAncestorGeometryClip ?
+                CreateLayerGeometryClip(node) :
+                null;
+        }
+
+        private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer)
+        {
+            node.LayerRoot = layer.LayerRoot;
+
+            layer.Dirty.Add(node.Bounds);
+            oldLayer.Dirty.Add(node.Bounds);
+
+            foreach (VisualNode child in node.Children)
+            {
+                // If the child is not the start of a new layer, recurse.
+                if (child.LayerRoot != child.Visual)
+                {
+                    PropagateLayer(child, layer, oldLayer);
+                }
+            }
+        }
+
+        private static IGeometryImpl CreateLayerGeometryClip(VisualNode node)
+        {
+            IGeometryImpl result = null;
+
+            for (;;)
+            {
+                node = (VisualNode)node.Parent;
+
+                if (node == null || (node.GeometryClip == null && !node.HasAncestorGeometryClip))
+                {
+                    break;
+                }
+
+                if (node?.GeometryClip != null)
+                {
+                    var transformed = node.GeometryClip.WithTransform(node.Transform);
+
+                    result = result == null ? transformed : result.Intersect(transformed);
+                }
+            }
+
+            return result;
+        }
+    }
+}

+ 75 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs

@@ -0,0 +1,75 @@
+using System;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Represents a layer in a <see cref="Scene"/>.
+    /// </summary>
+    public class SceneLayer
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SceneLayer"/> class.
+        /// </summary>
+        /// <param name="layerRoot">The visual at the root of the layer.</param>
+        /// <param name="distanceFromRoot">The distance from the scene root.</param>
+        public SceneLayer(IVisual layerRoot, int distanceFromRoot)
+        {
+            LayerRoot = layerRoot;
+            Dirty = new DirtyRects();
+            DistanceFromRoot = distanceFromRoot;
+        }
+
+        /// <summary>
+        /// Clones the layer.
+        /// </summary>
+        /// <returns>The cloned layer.</returns>
+        public SceneLayer Clone()
+        {
+            return new SceneLayer(LayerRoot, DistanceFromRoot)
+            {
+                Opacity = Opacity,
+                OpacityMask = OpacityMask,
+                OpacityMaskRect = OpacityMaskRect,
+                GeometryClip = GeometryClip,
+            };
+        }
+
+        /// <summary>
+        /// Gets the visual at the root of the layer.
+        /// </summary>
+        public IVisual LayerRoot { get; }
+
+        /// <summary>
+        /// Gets the distance of the layer root from the root of the scene.
+        /// </summary>
+        public int DistanceFromRoot { get; }
+
+        /// <summary>
+        /// Gets or sets the opacity of the layer.
+        /// </summary>
+        public double Opacity { get; set; } = 1;
+
+        /// <summary>
+        /// Gets or sets the opacity mask for the layer.
+        /// </summary>
+        public IBrush OpacityMask { get; set; }
+
+        /// <summary>
+        /// Gets or sets the target rectangle for the layer opacity mask.
+        /// </summary>
+        public Rect OpacityMaskRect { get; set; }
+
+        /// <summary>
+        /// Gets the layer's geometry clip.
+        /// </summary>
+        public IGeometryImpl GeometryClip { get; set; }
+
+        /// <summary>
+        /// Gets the dirty rectangles for the layer.
+        /// </summary>
+        internal DirtyRects Dirty { get; }
+    }
+}

+ 199 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs

@@ -0,0 +1,199 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// Holds a collection of layers for a <see cref="Scene"/>.
+    /// </summary>
+    public class SceneLayers : IEnumerable<SceneLayer>
+    {
+        private readonly IVisual _root;
+        private readonly List<SceneLayer> _inner = new List<SceneLayer>();
+        private readonly Dictionary<IVisual, SceneLayer> _index = new Dictionary<IVisual, SceneLayer>();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SceneLayers"/> class.
+        /// </summary>
+        /// <param name="root">The scene's root visual.</param>
+        public SceneLayers(IVisual root)
+        {
+            _root = root;
+        }
+
+        /// <summary>
+        /// Gets the number of layers in the scene.
+        /// </summary>
+        public int Count => _inner.Count;
+
+        /// <summary>
+        /// Gets a value indicating whether any of the layers have a dirty region.
+        /// </summary>
+        public bool HasDirty
+        {
+            get
+            {
+                foreach (var layer in _inner)
+                {
+                    if (!layer.Dirty.IsEmpty)
+                    {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Gets a layer by index.
+        /// </summary>
+        /// <param name="index">The index of the layer.</param>
+        /// <returns>The layer.</returns>
+        public SceneLayer this[int index] => _inner[index];
+
+        /// <summary>
+        /// Gets a layer by its root visual.
+        /// </summary>
+        /// <param name="visual">The layer's root visual.</param>
+        /// <returns>The layer.</returns>
+        public SceneLayer this[IVisual visual] => _index[visual];
+
+        /// <summary>
+        /// Adds a layer to the scene.
+        /// </summary>
+        /// <param name="layerRoot">The root visual of the layer.</param>
+        /// <returns>The created layer.</returns>
+        public SceneLayer Add(IVisual layerRoot)
+        {
+            Contract.Requires<ArgumentNullException>(layerRoot != null);
+
+            var distance = layerRoot.CalculateDistanceFromAncestor(_root);
+            var layer = new SceneLayer(layerRoot, distance);
+            var insert = FindInsertIndex(layer);
+            _index.Add(layerRoot, layer);
+            _inner.Insert(insert, layer);
+            return layer;
+        }
+
+        /// <summary>
+        /// Makes a deep clone of the layers.
+        /// </summary>
+        /// <returns>The cloned layers.</returns>
+        public SceneLayers Clone()
+        {
+            var result = new SceneLayers(_root);
+
+            foreach (var src in _inner)
+            {
+                var dest = src.Clone();
+                result._index.Add(dest.LayerRoot, dest);
+                result._inner.Add(dest);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Tests whether a layer exists with the specified root visual.
+        /// </summary>
+        /// <param name="layerRoot">The root visual.</param>
+        /// <returns>
+        /// True if a layer exists with the specified root visual, otherwise false.
+        /// </returns>
+        public bool Exists(IVisual layerRoot)
+        {
+            Contract.Requires<ArgumentNullException>(layerRoot != null);
+
+            return _index.ContainsKey(layerRoot);
+        }
+
+        /// <summary>
+        /// Tries to find a layer with the specified root visual.
+        /// </summary>
+        /// <param name="layerRoot">The root visual.</param>
+        /// <returns>The layer if found, otherwise null.</returns>
+        public SceneLayer Find(IVisual layerRoot)
+        {
+            SceneLayer result;
+            _index.TryGetValue(layerRoot, out result);
+            return result;
+        }
+
+        /// <summary>
+        /// Gets an existing layer or creates a new one if no existing layer is found.
+        /// </summary>
+        /// <param name="layerRoot">The root visual.</param>
+        /// <returns>The layer.</returns>
+        public SceneLayer GetOrAdd(IVisual layerRoot)
+        {
+            Contract.Requires<ArgumentNullException>(layerRoot != null);
+
+            SceneLayer result;
+
+            if (!_index.TryGetValue(layerRoot, out result))
+            {
+                result = Add(layerRoot);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Removes a layer from the scene.
+        /// </summary>
+        /// <param name="layerRoot">The root visual.</param>
+        /// <returns>True if a matching layer was removed, otherwise false.</returns>
+        public bool Remove(IVisual layerRoot)
+        {
+            Contract.Requires<ArgumentNullException>(layerRoot != null);
+
+            SceneLayer layer;
+
+            if (_index.TryGetValue(layerRoot, out layer))
+            {
+                Remove(layer);
+            }
+
+            return layer != null;
+        }
+
+        /// <summary>
+        /// Removes a layer from the scene.
+        /// </summary>
+        /// <param name="layer">The layer.</param>
+        /// <returns>True if the layer was part of the scene, otherwise false.</returns>
+        public bool Remove(SceneLayer layer)
+        {
+            Contract.Requires<ArgumentNullException>(layer != null);
+
+            _index.Remove(layer.LayerRoot);
+            return _inner.Remove(layer);
+        }
+
+        /// <inheritdoc/>
+        public IEnumerator<SceneLayer> GetEnumerator() => _inner.GetEnumerator();
+
+        /// <inheritdoc/>
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        private int FindInsertIndex(SceneLayer insert)
+        {
+            var index = 0;
+
+            foreach (var layer in _inner)
+            {
+                if (layer.DistanceFromRoot > insert.DistanceFromRoot)
+                {
+                    break;
+                }
+
+                ++index;
+            }
+
+            return index;
+        }
+    }
+}

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

@@ -0,0 +1,96 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the scene graph which represents a text draw.
+    /// </summary>
+    internal class TextNode : BrushDrawOperation
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TextNode"/> class.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <param name="foreground">The foreground brush.</param>
+        /// <param name="origin">The draw origin.</param>
+        /// <param name="text">The text to draw.</param>
+        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        public TextNode(
+            Matrix transform,
+            IBrush foreground,
+            Point origin,
+            IFormattedTextImpl text,
+            IDictionary<IVisual, Scene> childScenes = null)
+        {
+            Bounds = new Rect(origin, text.Size).TransformToAABB(transform);
+            Transform = transform;
+            Foreground = foreground?.ToImmutable();
+            Origin = origin;
+            Text = text;
+            ChildScenes = childScenes;
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds { get; }
+
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
+        /// <summary>
+        /// Gets the foreground brush.
+        /// </summary>
+        public IBrush Foreground { get; }
+
+        /// <summary>
+        /// Gets the draw origin.
+        /// </summary>
+        public Point Origin { get; }
+
+        /// <summary>
+        /// Gets the text to draw.
+        /// </summary>
+        public IFormattedTextImpl Text { get; }
+
+        /// <inheritdoc/>
+        public override IDictionary<IVisual, Scene> ChildScenes { get; }
+
+        /// <inheritdoc/>
+        public override void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            context.DrawText(Foreground, Origin, Text);
+        }
+
+        /// <summary>
+        /// Determines if this draw operation equals another.
+        /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
+        /// <param name="foreground">The foregroundof the other draw operation.</param>
+        /// <param name="origin">The draw origin of the other draw operation.</param>
+        /// <param name="text">The text of the other draw operation.</param>
+        /// <returns>True if the draw operations are the same, otherwise false.</returns>
+        /// <remarks>
+        /// The properties of the other draw operation are passed in as arguments to prevent
+        /// allocation of a not-yet-constructed draw operation object.
+        /// </remarks>
+        internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
+        {
+            return transform == Transform &&
+                Equals(foreground, Foreground) &&
+                origin == Origin &&
+                Equals(text, Text);
+        }
+
+        /// <inheritdoc/>
+        public override bool HitTest(Point p) => Bounds.Contains(p);
+    }
+}

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

@@ -0,0 +1,285 @@
+// 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.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    /// <summary>
+    /// A node in the low-level scene graph representing an <see cref="IVisual"/>.
+    /// </summary>
+    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.
+        /// </summary>
+        /// <param name="visual">The visual that this node represents.</param>
+        /// <param name="parent">The parent scene graph node, if any.</param>
+        public VisualNode(IVisual visual, IVisualNode parent)
+        {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
+            Visual = visual;
+            Parent = parent;
+            HasAncestorGeometryClip = parent != null && 
+                (parent.HasAncestorGeometryClip || parent.GeometryClip != null);
+        }
+
+        /// <inheritdoc/>
+        public IVisual Visual { get; }
+
+        /// <inheritdoc/>
+        public IVisualNode Parent { get; }
+
+        /// <inheritdoc/>
+        public Matrix Transform { get; set; }
+
+        /// <inheritdoc/>
+        public Rect Bounds => _bounds ?? CalculateBounds();
+
+        /// <inheritdoc/>
+        public Rect ClipBounds { get; set; }
+
+        /// <inheritdoc/>
+        public bool ClipToBounds { get; set; }
+
+        /// <inheritdoc/>
+        public IGeometryImpl GeometryClip { get; set; }
+
+        /// <inheritdoc/>
+        public bool HasAncestorGeometryClip { get; }
+
+        /// <summary>
+        /// Gets or sets the opacity of the scene graph node.
+        /// </summary>
+        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 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; }
+
+        public IVisual LayerRoot { get; set; }
+
+        /// <inheritdoc/>
+        public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren;
+
+        /// <inheritdoc/>
+        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
+        /// </summary>
+        /// <param name="parent">The new parent node.</param>
+        /// <returns>A cloned node.</returns>
+        public VisualNode Clone(IVisualNode parent)
+        {
+            return new VisualNode(Visual, parent)
+            {
+                Transform = Transform,
+                ClipBounds = ClipBounds,
+                ClipToBounds = ClipToBounds,
+                GeometryClip = GeometryClip,
+                _opacity = Opacity,
+                OpacityMask = OpacityMask,
+                _drawOperations = _drawOperations,
+                _drawOperationsCloned = true,
+                LayerRoot= LayerRoot,
+            };
+        }
+
+        /// <inheritdoc/>
+        public bool HitTest(Point p)
+        {
+            foreach (var operation in DrawOperations)
+            {
+                if (operation.HitTest(p) == true)
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public void BeginRender(IDrawingContextImpl context)
+        {
+            if (ClipToBounds)
+            {
+                context.Transform = Matrix.Identity;
+                context.PushClip(ClipBounds);
+            }
+
+            context.Transform = Transform;
+
+            if (GeometryClip != null)
+            {
+                context.PushGeometryClip(GeometryClip);
+            }
+        }
+
+        /// <inheritdoc/>
+        public void EndRender(IDrawingContextImpl context)
+        {
+            if (GeometryClip != null)
+            {
+                context.PopGeometryClip();
+            }
+
+            if (ClipToBounds)
+            {
+                context.PopClip();
+            }
+        }
+
+        private Rect CalculateBounds()
+        {
+            var result = new Rect();
+
+            foreach (var operation in DrawOperations)
+            {
+                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;
+            }
+        }
+    }
+}

+ 11 - 0
src/Avalonia.Visuals/Vector.cs

@@ -74,6 +74,17 @@ namespace Avalonia
             return new Vector(vector._x * scale, vector._y * scale);
         }
 
+        /// <summary>
+        /// Scales a vector.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <param name="scale">The divisor.</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector operator /(Vector vector, double scale)
+        {
+            return new Vector(vector._x / scale, vector._y / scale);
+        }
+
         /// <summary>
         /// Length of the vector
         /// </summary>

+ 24 - 0
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@@ -13,6 +13,30 @@ namespace Avalonia.VisualTree
     /// </summary>
     public static class VisualExtensions
     {
+        /// <summary>
+        /// Calculates the distance from a visual's <see cref="IRenderRoot"/>.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <param name="ancestor">The ancestor visual.</param>
+        /// <returns>
+        /// The number of steps from the visual to the ancestor or -1 if
+        /// <paramref name="visual"/> is not a descendent of <paramref name="ancestor"/>.
+        /// </returns>
+        public static int CalculateDistanceFromAncestor(this IVisual visual, IVisual ancestor)
+        {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
+            var result = 0;
+
+            while (visual != null && visual != ancestor)
+            {
+                ++result;
+                visual = visual.VisualParent;
+            }
+
+            return visual != null ? result : -1;
+        }
+
         /// <summary>
         /// Tries to get the first common ancestor of two visuals.
         /// </summary>

+ 0 - 26
src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject

@@ -1,26 +0,0 @@
-<ProjectConfiguration>
-  <AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
-  <BuildPriority>1000</BuildPriority>
-  <CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
-  <ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
-  <PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
-  <AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
-  <AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
-  <AllowCodeAnalysis>false</AllowCodeAnalysis>
-  <IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
-  <RunPreBuildEvents>false</RunPreBuildEvents>
-  <RunPostBuildEvents>false</RunPostBuildEvents>
-  <PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
-  <InstrumentAssembly>true</InstrumentAssembly>
-  <PreventSigningOfAssembly>false</PreventSigningOfAssembly>
-  <AnalyseExecutionTimes>true</AnalyseExecutionTimes>
-  <DetectStackOverflow>true</DetectStackOverflow>
-  <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
-  <DefaultTestTimeout>60000</DefaultTestTimeout>
-  <UseBuildConfiguration />
-  <UseBuildPlatform />
-  <ProxyProcessPath />
-  <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
-  <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
-  <BuildProcessArchitecture>x86</BuildProcessArchitecture>
-</ProjectConfiguration>

+ 1 - 0
src/Gtk/Avalonia.Cairo/CairoPlatform.cs

@@ -26,6 +26,7 @@ namespace Avalonia.Cairo
 {
     using System.IO;
     using global::Cairo;
+    using Rendering;
 
     public class CairoPlatform : IPlatformRenderInterface
     {

+ 3 - 2
src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs

@@ -10,9 +10,10 @@ namespace Avalonia.Cairo
 		{
 			var center = brush.Center.ToPixels(destinationSize);
 			var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize);
-            var radius = brush.Radius;
+            var radius = brush.Radius * Math.Min(destinationSize.Width, destinationSize.Height);
 
-			this.PlatformBrush = new RadialGradient(center.X, center.Y, radius, gradientOrigin.X, gradientOrigin.Y, radius);
+            this.PlatformBrush = new RadialGradient(center.X, center.Y, 1, gradientOrigin.X, gradientOrigin.Y, radius);
+            this.PlatformBrush.Matrix = Matrix.Identity.ToCairo();
 
             foreach (var stop in brush.GradientStops)
             {

+ 1 - 7
src/Gtk/Avalonia.Gtk/GtkPlatform.cs

@@ -28,7 +28,7 @@ namespace Avalonia.Gtk
     using Rendering;
     using Gtk = global::Gtk;
 
-    public class GtkPlatform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IRendererFactory
+    public class GtkPlatform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
     {
         private static readonly GtkPlatform s_instance = new GtkPlatform();
         private static Thread _uiThread;
@@ -53,7 +53,6 @@ namespace Avalonia.Gtk
                 .Bind<IKeyboardDevice>().ToConstant(GtkKeyboardDevice.Instance)
                 .Bind<IPlatformSettings>().ToConstant(s_instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
-                .Bind<IRendererFactory>().ToConstant(s_instance)
                 .Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
                 .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
                 .Bind<IPlatformIconLoader>().ToConstant(s_instance);
@@ -112,11 +111,6 @@ namespace Avalonia.Gtk
             return new PopupImpl();
         }
 
-        public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
-        {
-            return new ImmediateRenderer(root);
-        }
-
         public IWindowIconImpl LoadIcon(string fileName)
         {
             return new IconImpl(new Gdk.Pixbuf(fileName));

+ 6 - 0
src/Gtk/Avalonia.Gtk/TopLevelImpl.cs

@@ -10,6 +10,7 @@ using Gdk;
 using Action = System.Action;
 using WindowEdge = Avalonia.Controls.WindowEdge;
 using GLib;
+using Avalonia.Rendering;
 
 namespace Avalonia.Gtk
 {
@@ -141,6 +142,11 @@ namespace Avalonia.Gtk
             return new PopupImpl();
         }
 
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            return new ImmediateRenderer(root);
+        }
+
         public void Invalidate(Rect rect)
         {
             if (_widget?.GdkWindow != null)

+ 1 - 7
src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs

@@ -15,7 +15,7 @@ using Avalonia.Gtk3;
 
 namespace Avalonia.Gtk3
 {
-    public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface, IRendererFactory
+    public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
     {
         internal static readonly Gtk3Platform Instance = new Gtk3Platform();
         internal static readonly MouseDevice Mouse = new MouseDevice();
@@ -38,7 +38,6 @@ namespace Avalonia.Gtk3
                 .Bind<IPlatformThreadingInterface>().ToConstant(Instance)
                 .Bind<ISystemDialogImpl>().ToSingleton<SystemDialog>()
                 .Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
-                .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoader());
 
         }
@@ -52,11 +51,6 @@ namespace Avalonia.Gtk3
 
         public IPopupImpl CreatePopup() => new PopupImpl();
 
-        public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
-        {
-            return new ImmediateRenderer(root);
-        }
-
         public Size DoubleClickSize => new Size(4, 4);
 
         public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(100); //STUB

+ 6 - 0
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@@ -9,6 +9,7 @@ using Avalonia.Gtk3.Interop;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 
 namespace Avalonia.Gtk3
 {
@@ -354,5 +355,10 @@ namespace Avalonia.Gtk3
 
         IntPtr IPlatformHandle.Handle => Native.GetNativeGdkWindowHandle(Native.GtkWidgetGetWindow(GtkWidget));
         public IEnumerable<object> Surfaces => new object[] {Handle, _framebuffer};
+
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            return new ImmediateRenderer(root);
+        }
     }
 }

+ 6 - 0
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -5,6 +5,7 @@ using System.Text;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using Avalonia.Threading;
 
 namespace Avalonia.LinuxFramebuffer
@@ -24,6 +25,11 @@ namespace Avalonia.LinuxFramebuffer
             mice.Event += e => Input?.Invoke(e);
         }
 
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            return new ImmediateRenderer(root);
+        }
+
         public void Dispose()
         {
             throw new NotSupportedException();

+ 0 - 1
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -34,7 +34,6 @@ namespace Avalonia.LinuxFramebuffer
                 .Bind<IStandardCursorFactory>().ToTransient<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
-                .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
                 .Bind<IRenderLoop>().ToConstant(PlatformThreadingInterface.Instance);
         }

+ 4 - 4
src/Markup/Avalonia.Markup/Data/IndexerNode.cs

@@ -33,8 +33,8 @@ namespace Avalonia.Markup.Data
 
             if (incc != null)
             {
-                inputs.Add(WeakObservable.FromEventPattern<NotifyCollectionChangedEventArgs>(
-                    target,
+                inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
+                    incc,
                     nameof(incc.CollectionChanged))
                     .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
                     .Select(_ => GetValue(target)));
@@ -42,8 +42,8 @@ namespace Avalonia.Markup.Data
 
             if (inpc != null)
             {
-                inputs.Add(WeakObservable.FromEventPattern<PropertyChangedEventArgs>(
-                    target,
+                inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
+                    inpc,
                     nameof(inpc.PropertyChanged))
                     .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
                     .Select(_ => GetValue(target)));

+ 9 - 1
src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -13,7 +13,15 @@ namespace Avalonia.Markup.Data.Plugins
     public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
     {
         /// <inheritdoc/>
-        public bool Match(WeakReference reference) => reference.Target is AvaloniaObject;
+        public bool Match(object obj, string propertyName)
+        {
+            if (obj is AvaloniaObject a)
+            {
+                return AvaloniaPropertyRegistry.Instance.FindRegistered(a, propertyName) != null;
+            }
+
+            return false;
+        }
 
         /// <summary>
         /// Starts monitoring the value of a property on an object.

+ 4 - 3
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs

@@ -14,9 +14,10 @@ namespace Avalonia.Markup.Data.Plugins
         /// <summary>
         /// Checks whether this plugin can handle accessing the properties of the specified object.
         /// </summary>
-        /// <param name="reference">A weak reference to the object.</param>
-        /// <returns>True if the plugin can handle the object; otherwise false.</returns>
-        bool Match(WeakReference reference);
+        /// <param name="obj">The object.</param>
+        /// <param name="propertyName">The property name.</param>
+        /// <returns>True if the plugin can handle the property on the object; otherwise false.</returns>
+        bool Match(object obj, string propertyName);
 
         /// <summary>
         /// Starts monitoring the value of a property on an object.

+ 3 - 3
src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Markup.Data.Plugins
     public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
     {
         /// <inheritdoc/>
-        public bool Match(WeakReference reference) => true;
+        public bool Match(object obj, string propertyName) => true;
 
         /// <summary>
         /// Starts monitoring the value of a property on an object.
@@ -36,7 +36,7 @@ namespace Avalonia.Markup.Data.Plugins
             Contract.Requires<ArgumentNullException>(propertyName != null);
 
             var instance = reference.Target;
-            var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName);
+            var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName);
 
             if (p != null)
             {
@@ -138,7 +138,7 @@ namespace Avalonia.Markup.Data.Plugins
 
                 if (inpc != null)
                 {
-                    WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
+                    WeakSubscriptionManager.Subscribe(
                         inpc,
                         nameof(inpc.PropertyChanged),
                         this);

+ 1 - 1
src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs

@@ -37,7 +37,7 @@ namespace Avalonia.Markup.Data
 
         protected override IObservable<object> StartListeningCore(WeakReference reference)
         {
-            var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
+            var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
             var accessor = plugin?.Start(reference, PropertyName);
 
             if (_enableValidation && Next == null)

+ 1 - 0
src/Shared/SharedAssemblyInfo.cs

@@ -3,6 +3,7 @@
 
 using System.Reflection;
 using System.Resources;
+using System.Runtime.CompilerServices;
 
 [assembly: AssemblyCompany("")]
 [assembly: AssemblyConfiguration("")]

+ 0 - 26
src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject

@@ -1,26 +0,0 @@
-<ProjectConfiguration>
-  <AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
-  <BuildPriority>1000</BuildPriority>
-  <CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
-  <ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
-  <PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
-  <AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
-  <AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
-  <AllowCodeAnalysis>false</AllowCodeAnalysis>
-  <IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
-  <RunPreBuildEvents>false</RunPreBuildEvents>
-  <RunPostBuildEvents>false</RunPostBuildEvents>
-  <PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
-  <InstrumentAssembly>true</InstrumentAssembly>
-  <PreventSigningOfAssembly>false</PreventSigningOfAssembly>
-  <AnalyseExecutionTimes>true</AnalyseExecutionTimes>
-  <DetectStackOverflow>true</DetectStackOverflow>
-  <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
-  <DefaultTestTimeout>60000</DefaultTestTimeout>
-  <UseBuildConfiguration />
-  <UseBuildPlatform />
-  <ProxyProcessPath />
-  <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
-  <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
-  <BuildProcessArchitecture>x86</BuildProcessArchitecture>
-</ProjectConfiguration>

+ 0 - 26
src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject

@@ -1,26 +0,0 @@
-<ProjectConfiguration>
-  <AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
-  <BuildPriority>1000</BuildPriority>
-  <CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
-  <ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
-  <PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
-  <AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
-  <AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
-  <AllowCodeAnalysis>false</AllowCodeAnalysis>
-  <IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
-  <RunPreBuildEvents>false</RunPreBuildEvents>
-  <RunPostBuildEvents>false</RunPostBuildEvents>
-  <PreviouslyBuiltSuccessfully>false</PreviouslyBuiltSuccessfully>
-  <InstrumentAssembly>true</InstrumentAssembly>
-  <PreventSigningOfAssembly>false</PreventSigningOfAssembly>
-  <AnalyseExecutionTimes>true</AnalyseExecutionTimes>
-  <DetectStackOverflow>true</DetectStackOverflow>
-  <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
-  <DefaultTestTimeout>60000</DefaultTestTimeout>
-  <UseBuildConfiguration />
-  <UseBuildPlatform />
-  <ProxyProcessPath />
-  <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
-  <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
-  <BuildProcessArchitecture>x86</BuildProcessArchitecture>
-</ProjectConfiguration>

+ 0 - 172
src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs

@@ -1,172 +0,0 @@
-using System;
-using Avalonia.Media;
-using Avalonia.Platform;
-using SkiaSharp;
-#if WIN32
-using Avalonia.Win32.Interop;
-#endif
-
-namespace Avalonia.Skia
-{
-    internal partial class RenderTarget : IRenderTarget
-    {
-        public SKSurface Surface { get; protected set; }
-
-        public virtual DrawingContext CreateDrawingContext()
-        {
-            return
-                new DrawingContext(
-                    new DrawingContextImpl(Surface.Canvas));
-        }
-
-        public void Dispose()
-        {
-            // Nothing to do here.
-        }
-    }
-
-    internal class WindowRenderTarget : RenderTarget
-    {
-        private readonly IPlatformHandle _hwnd;
-        SKBitmap _bitmap;
-        int Width { get; set; }
-        int Height { get; set; }
-
-        public WindowRenderTarget(IPlatformHandle hwnd)
-        {
-            _hwnd = hwnd;
-            FixSize();
-        }
-
-        private void FixSize()
-        {
-            int width, height;
-            GetPlatformWindowSize(out width, out height);
-            if (Width == width && Height == height)
-                return;
-
-            Width = width;
-            Height = height;
-
-            if (Surface != null)
-            {
-                Surface.Dispose();
-            }
-
-            if (_bitmap != null)
-            {
-                _bitmap.Dispose();
-            }
-
-            _bitmap = new SKBitmap(width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
-
-            IntPtr length;
-            var pixels = _bitmap.GetPixels(out length);
-
-            // Wrap the bitmap in a Surface and keep it cached
-            Surface = SKSurface.Create(_bitmap.Info, pixels, _bitmap.RowBytes);
-        }
-
-        private void GetPlatformWindowSize(out int w, out int h)
-        {
-#if WIN32
-            UnmanagedMethods.RECT rc;
-            UnmanagedMethods.GetClientRect(_hwnd.Handle, out rc);
-            w = rc.right - rc.left;
-            h = rc.bottom - rc.top;
-#else
-			throw new NotImplementedException();
-#endif
-        }
-
-        private Size GetWindowDpiWin32()
-        {
-            if (UnmanagedMethods.ShCoreAvailable)
-            {
-                uint dpix, dpiy;
-
-                var monitor = UnmanagedMethods.MonitorFromWindow(
-                    _hwnd.Handle,
-                    UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
-
-                if (UnmanagedMethods.GetDpiForMonitor(
-                        monitor,
-                        UnmanagedMethods.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI,
-                        out dpix,
-                        out dpiy) == 0)
-                {
-                    return new Size(dpix, dpiy);
-                }
-            }
-
-            return new Size(96, 96);
-        }
-
-        public override DrawingContext CreateDrawingContext()
-        {
-            FixSize();
-
-            var canvas = Surface.Canvas;
-            canvas.RestoreToCount(0);
-            canvas.Save();
-            canvas.Clear(SKColors.Red);
-            canvas.ResetMatrix();
-
-            double scale = 1.0;
-
-            var runtimeService = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
-
-            if (runtimeService != null)
-            {
-                switch (runtimeService.GetRuntimeInfo().OperatingSystem)
-                {
-                    case OperatingSystemType.WinNT:
-                        var dpi = GetWindowDpiWin32();
-                        scale = dpi.Width / 96.0;
-                        break;
-                }
-            }
-
-            var result =
-                new DrawingContext(
-                    new WindowDrawingContextImpl(this), Matrix.CreateScale(scale, scale));
-            
-            return result;
-        }
-
-        public void Present()
-        {
-            _bitmap.LockPixels();
-            IntPtr length;
-            var pixels = _bitmap.GetPixels(out length);
-
-#if WIN32
-            UnmanagedMethods.BITMAPINFO bmi = new UnmanagedMethods.BITMAPINFO();
-            bmi.biSize = UnmanagedMethods.SizeOf_BITMAPINFOHEADER;
-            bmi.biWidth = _bitmap.Width;
-            bmi.biHeight = -_bitmap.Height; // top-down image
-            bmi.biPlanes = 1;
-            bmi.biBitCount = 32;
-            bmi.biCompression = (uint)UnmanagedMethods.BitmapCompressionMode.BI_RGB;
-            bmi.biSizeImage = 0;
-
-            IntPtr hdc = UnmanagedMethods.GetDC(_hwnd.Handle);
-
-            int ret = UnmanagedMethods.SetDIBitsToDevice(hdc,
-                0, 0,
-                (uint)_bitmap.Width, (uint)_bitmap.Height,
-                0, 0,
-                0, (uint)_bitmap.Height,
-                pixels,
-                ref bmi,
-                (uint)UnmanagedMethods.DIBColorTable.DIB_RGB_COLORS);
-
-            UnmanagedMethods.ReleaseDC(_hwnd.Handle, hdc);
-#else
-            throw new NotImplementedException();
-#endif
-
-            _bitmap.UnlockPixels();
-        }
-    }
-}

+ 10 - 10
src/Skia/Avalonia.Skia/BitmapImpl.cs

@@ -1,9 +1,5 @@
 using System;
-using System.Collections.Generic;
 using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using SkiaSharp;
@@ -12,6 +8,8 @@ namespace Avalonia.Skia
 {
     class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl
     {
+        private Vector _dpi;
+
         public SKBitmap Bitmap { get; private set; }
 
         public BitmapImpl(SKBitmap bm)
@@ -19,12 +17,14 @@ namespace Avalonia.Skia
             Bitmap = bm;
             PixelHeight = bm.Height;
             PixelWidth = bm.Width;
+            _dpi = new Vector(96, 96);
         }
 
-        public BitmapImpl(int width, int height, PixelFormat? fmt = null)
+        public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null)
         {
             PixelHeight = height;
             PixelWidth = width;
+            _dpi = dpi;
             var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
             var runtime = AvaloniaLocator.Current?.GetService<IRuntimePlatform>()?.GetRuntimeInfo();
             if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux)
@@ -68,8 +68,8 @@ namespace Avalonia.Skia
         {
             private readonly SKSurface _surface;
 
-            public BitmapDrawingContext(SKBitmap bitmap, IVisualBrushRenderer visualBrushRenderer)
-                : this(CreateSurface(bitmap), visualBrushRenderer)
+            public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
+                : this(CreateSurface(bitmap), dpi, visualBrushRenderer)
             {
 
             }
@@ -83,8 +83,8 @@ namespace Avalonia.Skia
                 return rv;
             }
 
-            public BitmapDrawingContext(SKSurface surface, IVisualBrushRenderer visualBrushRenderer)
-                : base(surface.Canvas, visualBrushRenderer)
+            public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
+                : base(surface.Canvas, dpi, visualBrushRenderer)
             {
                 _surface = surface;
             }
@@ -98,7 +98,7 @@ namespace Avalonia.Skia
 
         public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
-            return new BitmapDrawingContext(Bitmap, visualBrushRenderer);
+            return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer);
         }
 
         public void Save(Stream stream)

+ 14 - 5
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -11,6 +11,7 @@ namespace Avalonia.Skia
 {
     internal class DrawingContextImpl : IDrawingContextImpl
     {
+        private readonly Vector _dpi;
         private readonly Matrix? _postTransform;
         private readonly IDisposable[] _disposables;
         private readonly IVisualBrushRenderer _visualBrushRenderer;
@@ -20,12 +21,13 @@ namespace Avalonia.Skia
 
         public DrawingContextImpl(
             SKCanvas canvas,
+            Vector dpi,
             IVisualBrushRenderer visualBrushRenderer,
-            Matrix? postTransform = null,
             params IDisposable[] disposables)
         {
-            if (postTransform.HasValue && !postTransform.Value.IsIdentity)
-                _postTransform = postTransform;
+            _dpi = dpi;
+            if (dpi.X != 96 || dpi.Y != 96)
+                _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
             _visualBrushRenderer = visualBrushRenderer;
             _disposables = disposables;
             Canvas = canvas;
@@ -49,6 +51,13 @@ namespace Avalonia.Skia
             }
         }
 
+        public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+        {
+            PushOpacityMask(opacityMask, opacityMaskRect);
+            DrawImage(source, 1, new Rect(0, 0, source.PixelWidth, source.PixelHeight), destRect);
+            PopOpacityMask();
+        }
+
         public void DrawLine(Pen pen, Point p1, Point p2)
         {
             using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
@@ -204,7 +213,7 @@ namespace Avalonia.Skia
 
                     if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
                     {
-                        var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height);
+                        var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
 
                         using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
                         {
@@ -229,7 +238,7 @@ namespace Avalonia.Skia
             if (tileBrush != null && tileBrushImage != null)
             {
                 var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
-                var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height);
+                var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi);
                 rv.AddDisposable(bitmap);
                 using (var context = bitmap.CreateDrawingContext(null))
                 {

+ 1 - 0
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -615,6 +615,7 @@ namespace Avalonia.Skia
 
             if (brush != null)
             {
+                brush = brush.ToImmutable();
                 _foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
             }
         }

+ 1 - 2
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -76,8 +76,7 @@ namespace Avalonia.Skia
             canvas.RestoreToCount(0);
             canvas.Save();
             canvas.ResetMatrix();
-            var scale = Matrix.CreateScale(fb.Dpi.X / 96, fb.Dpi.Y / 96);
-            return new DrawingContextImpl(canvas, visualBrushRenderer, scale, canvas, surface, shim, fb);
+            return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb);
         }
     }
 }

+ 2 - 2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Skia
             if (height < 1)
                 throw new ArgumentException("Height can't be less than 1", nameof(height));
 
-            return new BitmapImpl(width, height);
+            return new BitmapImpl(width, height, new Vector(dpiX, dpiY));
         }
 
         public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
@@ -90,7 +90,7 @@ namespace Avalonia.Skia
 
         public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)
         {
-            return new BitmapImpl(width, height, format);
+            return new BitmapImpl(width, height, new Vector(96, 96), format);
         }
     }
 }

+ 11 - 0
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@@ -90,6 +90,17 @@ namespace Avalonia.Skia
             }
         }
 
+        public static TextAlignment ToAvalonia(this SKTextAlign a)
+        {
+            switch (a)
+            {
+                default:
+                case SKTextAlign.Left: return TextAlignment.Left;
+                case SKTextAlign.Center: return TextAlignment.Center;
+                case SKTextAlign.Right: return TextAlignment.Right;
+            }
+        }
+
         public static SKPath Clone(this SKPath src)
         {
             return src != null ? new SKPath(src) : null;

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

@@ -191,4 +191,4 @@ namespace Avalonia.Direct2D1
             return new WicBitmapImpl(s_imagingFactory, format, data, width, height, stride);
         }
     }
-}
+}

+ 4 - 4
src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs

@@ -25,11 +25,11 @@ namespace Avalonia.Direct2D1.Media
             }).ToArray();
 
             var centerPoint = brush.Center.ToPixels(destinationSize);
-            var GradientOriginOffset = brush.GradientOrigin.ToPixels(destinationSize);
+            var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint;
             
             // Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property
-            var radiusX = brush.Radius;
-            var radiusY = brush.Radius;
+            var radiusX = brush.Radius * destinationSize.Width;
+            var radiusY = brush.Radius * destinationSize.Height;
 
             using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
                 target,
@@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.Media
                     new SharpDX.Direct2D1.RadialGradientBrushProperties
                     {
                         Center = centerPoint.ToSharpDX(),
-                        GradientOriginOffset = GradientOriginOffset.ToSharpDX(),
+                        GradientOriginOffset = gradientOrigin.ToSharpDX(),
                         RadiusX = (float)radiusX,
                         RadiusY = (float)radiusY
                     },

+ 6 - 0
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@@ -13,6 +13,7 @@ using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Layout;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using Key = Avalonia.Input.Key;
 using KeyEventArgs = System.Windows.Input.KeyEventArgs;
 using MouseButton = System.Windows.Input.MouseButton;
@@ -88,6 +89,11 @@ namespace Avalonia.Win32.Interop.Wpf
             _ttl.ScalingChanged?.Invoke(_ttl.Scaling);
         }
 
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            return new ImmediateRenderer(root);
+        }
+
         public void Dispose()
         {
             _ttl.Closed?.Invoke();

+ 1 - 1
src/Windows/Avalonia.Win32/FramebufferManager.cs

@@ -25,7 +25,7 @@ namespace Avalonia.Win32
             UnmanagedMethods.GetClientRect(_hwnd, out rc);
             var width = rc.right - rc.left;
             var height = rc.bottom - rc.top;
-            if (_fb == null || _fb.Width != width || _fb.Height != height)
+            if ((_fb == null || _fb.Width != width || _fb.Height != height) && width > 0 && height > 0)
             {
                 _fb?.Deallocate();
                 _fb = null;

+ 17 - 11
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -25,17 +25,21 @@ namespace Avalonia
 {
     public static class Win32ApplicationExtensions
     {
-        public static T UseWin32<T>(this T builder) where T : AppBuilderBase<T>, new()
+        public static T UseWin32<T>(
+            this T builder,
+            bool deferredRendering = true) 
+                where T : AppBuilderBase<T>, new()
         {
-            builder.UseWindowingSubsystem(Win32.Win32Platform.Initialize, "Win32");
-            return builder;
+            return builder.UseWindowingSubsystem(
+                () => Win32.Win32Platform.Initialize(deferredRendering),
+                "Win32");
         }
     }
 }
 
 namespace Avalonia.Win32
 {
-    partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IRendererFactory
+    partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
     {
         private static readonly Win32Platform s_instance = new Win32Platform();
         private static uint _uiThread;
@@ -54,6 +58,8 @@ namespace Avalonia.Win32
             CreateMessageWindow();
         }
 
+        public static bool UseDeferredRendering { get; set; }
+
         public Size DoubleClickSize => new Size(
             UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK),
             UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK));
@@ -61,6 +67,11 @@ namespace Avalonia.Win32
         public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(UnmanagedMethods.GetDoubleClickTime());
 
         public static void Initialize()
+        {
+            Initialize(true);
+        }
+
+        public static void Initialize(bool deferredRendering = true)
         {
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
@@ -69,11 +80,11 @@ namespace Avalonia.Win32
                 .Bind<IPlatformSettings>().ToConstant(s_instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop(60))
-                .Bind<IRendererFactory>().ToConstant(s_instance)
                 .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
                 .Bind<IWindowingPlatform>().ToConstant(s_instance)
                 .Bind<IPlatformIconLoader>().ToConstant(s_instance);
-            
+
+            UseDeferredRendering = deferredRendering;
             _uiThread = UnmanagedMethods.GetCurrentThreadId();
         }
 
@@ -197,10 +208,5 @@ namespace Avalonia.Win32
         {
             return new PopupImpl();
         }
-
-        public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
-        {
-            return new ImmediateRenderer(root);
-        }
     }
 }

+ 10 - 0
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -15,6 +15,7 @@ using Avalonia.Platform;
 using Avalonia.Win32.Input;
 using Avalonia.Win32.Interop;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
+using Avalonia.Rendering;
 #if NETSTANDARD
 using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception;
 #endif
@@ -90,6 +91,15 @@ namespace Avalonia.Win32
             }
         }
 
+
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
+            return Win32Platform.UseDeferredRendering ?
+                (IRenderer)new DeferredRenderer(root, loop) :
+                new ImmediateRenderer(root);
+        }
+
         public void Resize(Size value)
         {
             if (value != ClientSize)

+ 7 - 1
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@@ -18,6 +18,7 @@ using Avalonia.iOS.Specific;
 using ObjCRuntime;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Rendering;
 
 namespace Avalonia.iOS
 {
@@ -62,7 +63,12 @@ namespace Avalonia.iOS
         public Size ClientSize => Bounds.Size.ToAvalonia();
 
         public IMouseDevice MouseDevice => iOSPlatform.MouseDevice;
-        
+
+        public IRenderer CreateRenderer(IRenderRoot root)
+        {
+            return new ImmediateRenderer(root);
+        }
+
         public override void Draw(CGRect rect)
         {
             Paint?.Invoke(new Rect(rect.X, rect.Y, rect.Width, rect.Height));

+ 0 - 1
src/iOS/Avalonia.iOS/iOSPlatform.cs

@@ -57,7 +57,6 @@ namespace Avalonia.iOS
                 //.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
                 .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
-                .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
                 .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -317,6 +317,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         {
             public IRenderer Renderer { get; }
             public Size ClientSize { get; }
+            public double RenderScaling => 1;
 
             public IRenderTarget CreateRenderTarget()
             {

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@@ -1034,6 +1034,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         {
             public IRenderer Renderer { get; }
             public Size ClientSize { get; }
+            public double RenderScaling => 1;
 
             public IRenderTarget CreateRenderTarget()
             {

+ 56 - 2
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@@ -27,6 +27,8 @@ namespace Avalonia.Controls.UnitTests
             {
                 var impl = Mock.Of<IWindowBaseImpl>(x => x.Scaling == 1);
 
+                Mock.Get(impl).Setup(x => x.Resize(It.IsAny<Size>())).Callback(() => { });
+
                 var target = new TestWindowBase(impl)
                 {
                     Template = CreateTemplate(),
@@ -183,6 +185,56 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Showing_Should_Start_Renderer()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var renderer = new Mock<IRenderer>();
+                var target = new TestWindowBase(renderer.Object);
+
+                target.Show();
+
+                renderer.Verify(x => x.Start(), Times.Once);
+            }
+        }
+
+        [Fact]
+        public void Hiding_Should_Stop_Renderer()
+        {
+
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var renderer = new Mock<IRenderer>();
+                var target = new TestWindowBase(renderer.Object);
+
+                target.Show();
+                target.Hide();
+
+                renderer.Verify(x => x.Stop(), Times.Once);
+            }
+        }
+
+        [Fact]
+        public void Renderer_Should_Be_Disposed_When_Impl_Signals_Close()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var renderer = new Mock<IRenderer>();
+                var windowImpl = new Mock<IPopupImpl>();
+                windowImpl.Setup(x => x.Scaling).Returns(1);
+                windowImpl.SetupProperty(x => x.Closed);
+                windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
+
+                var target = new TestWindowBase(windowImpl.Object);
+
+                target.Show();
+                windowImpl.Object.Closed();
+
+                renderer.Verify(x => x.Dispose(), Times.Once);
+            }
+        }
+
         private FuncControlTemplate<TestWindowBase> CreateTemplate()
         {
             return new FuncControlTemplate<TestWindowBase>(x =>
@@ -197,8 +249,10 @@ namespace Avalonia.Controls.UnitTests
         {
             public bool IsClosed { get; private set; }
 
-            public TestWindowBase()
-                : base(Mock.Of<IWindowBaseImpl>(x => x.Scaling == 1))
+            public TestWindowBase(IRenderer renderer = null)
+                : base(Mock.Of<IWindowBaseImpl>(x => 
+                    x.Scaling == 1 &&
+                    x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer))
             {
             }
 

+ 53 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -4,8 +4,10 @@
 // </copyright>
 // -----------------------------------------------------------------------
 
+using System;
 using System.Collections.Generic;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -185,11 +187,62 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Showing_Should_Start_Renderer()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var renderer = new Mock<IRenderer>();
+                var target = new Window(CreateImpl(renderer));
+
+                target.Show();
+
+                renderer.Verify(x => x.Start(), Times.Once);
+            }
+        }
+
+        [Fact]
+        public void ShowDialog_Should_Start_Renderer()
+        {
+
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var renderer = new Mock<IRenderer>();
+                var target = new Window(CreateImpl(renderer));
+
+                target.Show();
+
+                renderer.Verify(x => x.Start(), Times.Once);
+            }
+        }
+
+        [Fact]
+        public void Hiding_Should_Stop_Renderer()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var renderer = new Mock<IRenderer>();
+                var target = new Window(CreateImpl(renderer));
+
+                target.Show();
+                target.Hide();
+
+                renderer.Verify(x => x.Stop(), Times.Once);
+            }
+        }
+
         private void ClearOpenWindows()
         {
             // HACK: We really need a decent way to have "statics" that can be scoped to
             // AvaloniaLocator scopes.
             ((IList<Window>)Window.OpenWindows).Clear();
         }
+
+        private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
+        {
+            return Mock.Of<IWindowImpl>(x =>
+                x.Scaling == 1 &&
+                x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
+        }
     }
 }

+ 29 - 0
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@@ -3,6 +3,35 @@
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
     <OutputType>Library</OutputType>
   </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <DocumentationFile>bin\Debug\Avalonia.Input.UnitTests.XML</DocumentationFile>
+    <NoWarn>CS1591</NoWarn>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+    <Reference Include="WindowsBase" />
+  </ItemGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />
   <Import Project="..\..\build\XUnit.props" />

+ 85 - 0
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@@ -0,0 +1,85 @@
+using Avalonia.Controls;
+using Avalonia.Input.Raw;
+using Avalonia.Layout;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using System;
+using Xunit;
+
+namespace Avalonia.Input.UnitTests
+{
+    public class MouseDeviceTests
+    {
+        [Fact]
+        public void MouseMove_Should_Update_PointerOver()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (TestApplication(renderer.Object))
+            {
+                var inputManager = InputManager.Instance;
+
+                Canvas canvas;
+                Border border;
+                Decorator decorator;
+
+                var root = new TestRoot
+                {
+                    MouseDevice = new MouseDevice(),
+                    Renderer = renderer.Object,
+                    Child = new Panel
+                    {
+                        Children =
+                        {
+                            (canvas = new Canvas()),
+                            (border = new Border
+                            {
+                                Child = decorator = new Decorator(),
+                            })
+                        }
+                    }
+                };
+
+                renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<Func<IVisual, bool>>()))
+                    .Returns(new[] { decorator });
+
+                inputManager.ProcessInput(new RawMouseEventArgs(
+                    root.MouseDevice,
+                    0,
+                    root,
+                    RawMouseEventType.Move,
+                    new Point(),
+                    InputModifiers.None));
+
+                Assert.True(decorator.IsPointerOver);
+                Assert.True(border.IsPointerOver);
+                Assert.False(canvas.IsPointerOver);
+                Assert.True(root.IsPointerOver);
+
+                renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<Func<IVisual, bool>>()))
+                    .Returns(new[] { canvas });
+
+                inputManager.ProcessInput(new RawMouseEventArgs(
+                    root.MouseDevice,
+                    0,
+                    root,
+                    RawMouseEventType.Move,
+                    new Point(),
+                    InputModifiers.None));
+
+                Assert.False(decorator.IsPointerOver);
+                Assert.False(border.IsPointerOver);
+                Assert.True(canvas.IsPointerOver);
+                Assert.True(root.IsPointerOver);
+            }
+        }
+
+        private IDisposable TestApplication(IRenderer renderer)
+        {
+            return UnitTestApplication.Start(
+                new TestServices(inputManager: new InputManager()));
+        }
+    }
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff