浏览代码

Merge pull request #956 from AvaloniaUI/port-immediaterenderer

Port ImmediateRenderer from scenegraph
Steven Kirk 8 年之前
父节点
当前提交
2eff412833
共有 58 个文件被更改,包括 1737 次插入1552 次删除
  1. 2 2
      samples/RenderTest/MainWindow.xaml.cs
  2. 0 41
      src/Avalonia.Base/AvaloniaDisposable.cs
  3. 35 1
      src/Avalonia.Controls/Control.cs
  4. 12 3
      src/Avalonia.Controls/TopLevel.cs
  5. 1 2
      src/Avalonia.Controls/WindowBase.cs
  6. 18 7
      src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs
  7. 7 1
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  8. 7 3
      src/Avalonia.Visuals/Platform/IRenderTarget.cs
  9. 38 1
      src/Avalonia.Visuals/Rendering/IRenderer.cs
  10. 20 0
      src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs
  11. 31 0
      src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs
  12. 278 0
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  13. 0 67
      src/Avalonia.Visuals/Rendering/Renderer.cs
  14. 51 0
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  15. 0 205
      src/Avalonia.Visuals/Rendering/RendererMixin.cs
  16. 193 0
      src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs
  17. 13 0
      src/Avalonia.Visuals/Rendering/ZIndexComparer.cs
  18. 2 2
      src/Avalonia.Visuals/Visual.cs
  19. 10 1
      src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs
  20. 0 2
      src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj
  21. 1 1
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  22. 77 49
      src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs
  23. 49 8
      src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs
  24. 2 2
      src/Gtk/Avalonia.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs
  25. 0 55
      src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs
  26. 0 15
      src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs
  27. 4 5
      src/Gtk/Avalonia.Cairo/RenderTarget.cs
  28. 1 1
      src/Gtk/Avalonia.Gtk/GtkPlatform.cs
  29. 5 2
      src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs
  30. 1 2
      src/Shared/RenderHelpers/RenderHelpers.projitems
  31. 0 209
      src/Shared/RenderHelpers/TileBrushImplHelper.cs
  32. 8 6
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  33. 57 10
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  34. 6 6
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  35. 7 9
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  36. 1 2
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  37. 1 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  38. 14 9
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  39. 79 26
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  40. 58 29
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  41. 14 6
      src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs
  42. 3 8
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  43. 5 6
      src/Windows/Avalonia.Direct2D1/RenderTarget.cs
  44. 9 9
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  45. 7 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  46. 0 509
      tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs
  47. 3 3
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  48. 0 13
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  49. 62 0
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  50. 152 0
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  51. 4 4
      tests/Avalonia.UnitTests/TestServices.cs
  52. 16 1
      tests/Avalonia.UnitTests/UnitTestApplication.cs
  53. 1 1
      tests/Avalonia.Visuals.UnitTests/RenderTests_Culling.cs
  54. 0 39
      tests/Avalonia.Visuals.UnitTests/TestRoot.cs
  55. 94 11
      tests/Avalonia.Visuals.UnitTests/VisualTests.cs
  56. 1 1
      tests/Avalonia.Visuals.UnitTests/VisualTree/BoundsTrackerTests.cs
  57. 1 1
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  58. 276 154
      tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

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

@@ -7,7 +7,6 @@ using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using RenderTest.ViewModels;
 using ReactiveUI;
-using Avalonia.Rendering;
 
 namespace RenderTest
 {
@@ -19,7 +18,8 @@ namespace RenderTest
             this.AttachDevTools();
 
             var vm = new MainWindowViewModel();
-            vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => RendererMixin.DrawFpsCounter = x);
+            vm.WhenAnyValue(x => x.DrawDirtyRects).Subscribe(x => Renderer.DrawDirtyRects = x);
+            vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => Renderer.DrawFps = x);
             this.DataContext = vm;
         }
 

+ 0 - 41
src/Avalonia.Base/AvaloniaDisposable.cs

@@ -1,41 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia.Platform;
-
-namespace Avalonia
-{
-    public abstract class AvaloniaDisposable : IDisposable
-    {
-#if DEBUG_DISPOSE
-        public string DisposedAt { get; private set; }
-#endif
-
-
-        public bool IsDisposed { get; private set; }
-
-        public void Dispose()
-        {
-            IsDisposed = true;
-#if DEBUG_DISPOSE
-            DisposedAt = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetStackTrace();
-#endif
-            DoDispose();
-        }
-
-        protected void CheckDisposed()
-        {
-            if (IsDisposed)
-                throw new ObjectDisposedException(GetType().FullName
-#if DEBUG_DISPOSE
-                    , "Disposed at: \n" + DisposedAt
-#endif
-
-                    );
-        }
-
-        protected abstract void DoDispose();
-    }
-}

+ 35 - 1
src/Avalonia.Controls/Control.cs

@@ -17,7 +17,9 @@ using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Logging;
 using Avalonia.LogicalTree;
+using Avalonia.Rendering;
 using Avalonia.Styling;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -33,7 +35,7 @@ namespace Avalonia.Controls
     /// - Implements <see cref="IStyleable"/> to allow styling to work on the control.
     /// - Implements <see cref="ILogical"/> to form part of a logical tree.
     /// </remarks>
-    public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize
+    public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize, IVisualBrushInitialize
     {
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -460,6 +462,38 @@ namespace Avalonia.Controls
             InheritanceParent = parent;
         }
 
+        /// <inheritdoc/>
+        void IVisualBrushInitialize.EnsureInitialized()
+        {
+            if (VisualRoot == null)
+            {
+                if (!IsInitialized)
+                {
+                    foreach (var i in this.GetSelfAndVisualDescendents())
+                    {
+                        var c = i as IControl;
+
+                        if (c?.IsInitialized == false)
+                        {
+                            var init = c as ISupportInitialize;
+
+                            if (init != null)
+                            {
+                                init.BeginInit();
+                                init.EndInit();
+                            }
+                        }
+                    }
+                }
+
+                if (!IsArrangeValid)
+                {
+                    Measure(Size.Infinity);
+                    Arrange(new Rect(DesiredSize));
+                }
+            }
+        }
+
         /// <summary>
         /// Adds a pseudo-class to be set when a property is true.
         /// </summary>

+ 12 - 3
src/Avalonia.Controls/TopLevel.cs

@@ -96,10 +96,10 @@ namespace Avalonia.Controls
 
             PlatformImpl.Closed = HandleClosed;
             PlatformImpl.Input = HandleInput;
-            PlatformImpl.Paint = Renderer != null ? (Action<Rect>)Renderer.Render : null;
+            PlatformImpl.Paint = HandlePaint;
             PlatformImpl.Resized = HandleResized;
             PlatformImpl.ScalingChanged = HandleScalingChanged;
-            
+
 
             _keyboardNavigationHandler?.SetOwner(this);
             _accessKeyHandler?.SetOwner(this);
@@ -208,6 +208,15 @@ namespace Avalonia.Controls
             return PlatformImpl.PointToScreen(p);
         }
 
+        /// <summary>
+        /// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
+        /// </summary>
+        /// <param name="rect">The dirty area.</param>
+        protected virtual void HandlePaint(Rect rect)
+        {
+            Renderer?.Paint(rect);
+        }
+
         /// <summary>
         /// Handles a closed notification from <see cref="ITopLevelImpl.Closed"/>.
         /// </summary>
@@ -229,7 +238,7 @@ namespace Avalonia.Controls
             Width = clientSize.Width;
             Height = clientSize.Height;
             LayoutManager.Instance.ExecuteLayoutPass();
-            PlatformImpl.Invalidate(new Rect(clientSize));
+            Renderer?.Resized(clientSize);
         }
 
         /// <summary>

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

@@ -210,8 +210,7 @@ namespace Avalonia.Controls
             }
             ClientSize = clientSize;
             LayoutManager.Instance.ExecuteLayoutPass();
-            PlatformImpl.Invalidate(new Rect(clientSize));
-
+            Renderer?.Resized(clientSize);
         }
 
         /// <summary>

+ 18 - 7
src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs

@@ -16,10 +16,12 @@ namespace Avalonia.Media.Imaging
         /// <summary>
         /// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
         /// </summary>
-        /// <param name="width">The width of the bitmap.</param>
-        /// <param name="height">The height of the bitmap.</param>
-        public RenderTargetBitmap(int width, int height)
-            : base(CreateImpl(width, height))
+        /// <param name="pixelWidth">The width of the bitmap in pixels.</param>
+        /// <param name="pixelHeight">The height of the bitmap in pixels.</param>
+        /// <param name="dpiX">The horizontal DPI of the bitmap.</param>
+        /// <param name="dpiY">The vertical DPI of the bitmap.</param>
+        public RenderTargetBitmap(int pixelWidth, int pixelHeight, double dpiX = 96, double dpiY = 96)
+            : base(CreateImpl(pixelWidth, pixelHeight, dpiX, dpiY))
         {
         }
 
@@ -36,18 +38,27 @@ namespace Avalonia.Media.Imaging
             PlatformImpl.Dispose();
         }
 
+        /// <summary>
+        /// Renders a visual to the <see cref="RenderTargetBitmap"/>.
+        /// </summary>
+        /// <param name="visual">The visual to render.</param>
+        public void Render(IVisual visual) => ImmediateRenderer.Render(visual, this);
+
         /// <summary>
         /// Creates a platform-specific imlementation for a <see cref="RenderTargetBitmap"/>.
         /// </summary>
         /// <param name="width">The width of the bitmap.</param>
         /// <param name="height">The height of the bitmap.</param>
+        /// <param name="dpiX">The horizontal DPI of the bitmap.</param>
+        /// <param name="dpiY">The vertical DPI of the bitmap.</param>
         /// <returns>The platform-specific implementation.</returns>
-        private static IBitmapImpl CreateImpl(int width, int height)
+        private static IBitmapImpl CreateImpl(int width, int height, double dpiX, double dpiY)
         {
             IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            return factory.CreateRenderTargetBitmap(width, height);
+            return factory.CreateRenderTargetBitmap(width, height, dpiX, dpiY);
         }
 
-        public DrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext();
+        /// <inheritdoc/>
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer vbr) => PlatformImpl.CreateDrawingContext(vbr);
     }
 }

+ 7 - 1
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -51,8 +51,14 @@ namespace Avalonia.Platform
         /// </summary>
         /// <param name="width">The width of the bitmap.</param>
         /// <param name="height">The height of the bitmap.</param>
+        /// <param name="dpiX">The horizontal DPI of the bitmap.</param>
+        /// <param name="dpiY">The vertical DPI of the bitmap.</param>
         /// <returns>An <see cref="IRenderTargetBitmapImpl"/>.</returns>
-        IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height);
+        IRenderTargetBitmapImpl CreateRenderTargetBitmap(
+            int width,
+            int height,
+            double dpiX,
+            double dpiY);
 
         /// <summary>
         /// Creates a writable bitmap implementation.

+ 7 - 3
src/Avalonia.Visuals/Platform/IRenderTarget.cs

@@ -2,7 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using Avalonia.Media;
+using Avalonia.Rendering;
 
 namespace Avalonia.Platform
 {
@@ -15,8 +15,12 @@ namespace Avalonia.Platform
     public interface IRenderTarget : IDisposable
     {
         /// <summary>
-        /// Creates an <see cref="DrawingContext"/> for a rendering session.
+        /// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
         /// </summary>
-        DrawingContext CreateDrawingContext();
+        /// <param name="visualBrushRenderer">
+        /// A render to be used to render visual brushes. May be null if no visual brushes are
+        /// to be drawn.
+        /// </param>
+        IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer);
     }
 }

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

@@ -3,13 +3,50 @@
 
 using System;
 using Avalonia.VisualTree;
+using System.Collections.Generic;
 
 namespace Avalonia.Rendering
 {
+    /// <summary>
+    /// Defines the interface for a renderer.
+    /// </summary>
     public interface IRenderer : IDisposable
     {
+        /// <summary>
+        /// Gets or sets a value indicating whether the renderer should draw an FPS counter.
+        /// </summary>
+        bool DrawFps { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the renderer should a visual representation
+        /// of its dirty rectangles.
+        /// </summary>
+        bool DrawDirtyRects { get; set; }
+
+        /// <summary>
+        /// Mark a visual as dirty and needing re-rendering.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
         void AddDirty(IVisual visual);
 
-        void Render(Rect rect);
+        /// <summary>
+        /// Hit tests a location to find the visuals at the specified point.
+        /// </summary>
+        /// <param name="p">The point, in client coordinates.</param>
+        /// <param name="filter">An optional filter.</param>
+        /// <returns>The visuals at the specified point, topmost first.</returns>
+        IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter);
+
+        /// <summary>
+        /// Called when a resize notification is received by the control being rendered.
+        /// </summary>
+        /// <param name="size">The new size of the window.</param>
+        void Resized(Size size);
+
+        /// <summary>
+        /// Called when a paint notification is received by the control being rendered.
+        /// </summary>
+        /// <param name="rect">The dirty rectangle.</param>
+        void Paint(Rect rect);
     }
 }

+ 20 - 0
src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs

@@ -0,0 +1,20 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// Internal interface for initializing controls that are to be used as the viusal in a
+    /// <see cref="VisualBrush"/>.
+    /// </summary>
+    public interface IVisualBrushInitialize
+    {
+        /// <summary>
+        /// Ensures that the control is ready to use as the visual in a visual brush.
+        /// </summary>
+        void EnsureInitialized();
+    }
+}

+ 31 - 0
src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.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 Avalonia.Media;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// Defines a renderer used to render a visual brush to a bitmap.
+    /// </summary>
+    public interface IVisualBrushRenderer
+    {
+        /// <summary>
+        /// Gets the size of the intermediate render target to which the visual brush should be
+        /// drawn.
+        /// </summary>
+        /// <param name="brush">The visual brush.</param>
+        /// <returns>The size of the intermediate render target to create.</returns>
+        Size GetRenderTargetSize(IVisualBrush brush);
+
+        /// <summary>
+        /// Renders a visual brush to a bitmap.
+        /// </summary>
+        /// <param name="context">The drawing context to render to.</param>
+        /// <param name="brush">The visual brush.</param>
+        /// <returns>A bitmap containing the rendered brush.</returns>
+        void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush);
+    }
+}

+ 278 - 0
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -0,0 +1,278 @@
+// 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;
+using Avalonia.VisualTree;
+using System.Collections.Generic;
+using Avalonia.Media;
+using System.Linq;
+
+namespace Avalonia.Rendering
+{
+    /// <summary>
+    /// A renderer which renders the state of the visual tree without an intermediate scene graph
+    /// representation.
+    /// </summary>
+    /// <remarks>
+    /// The immediate renderer supports only clip-bound-based hit testing; a control's geometry is
+    /// not taken into account.
+    /// </remarks>
+    public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
+    {
+        private readonly IVisual _root;
+        private readonly IRenderRoot _renderRoot;
+        private IRenderTarget _renderTarget;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImmediateRenderer"/> class.
+        /// </summary>
+        /// <param name="root">The control to render.</param>
+        public ImmediateRenderer(IVisual root)
+        {
+            Contract.Requires<ArgumentNullException>(root != null);
+
+            _root = root;
+            _renderRoot = root as IRenderRoot;
+        }
+
+        /// <inheritdoc/>
+        public bool DrawFps { get; set; }
+
+        /// <inheritdoc/>
+        public bool DrawDirtyRects { get; set; }
+
+        /// <inheritdoc/>
+        public void Paint(Rect rect)
+        {
+            if (_renderTarget == null)
+            {
+                _renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
+            }
+
+            try
+            {
+                using (var context = new DrawingContext(_renderTarget.CreateDrawingContext(this)))
+                {
+                    using (context.PushTransformContainer())
+                    {
+                        Render(context, _root, _root.Bounds);
+                    }
+
+                    if (DrawDirtyRects)
+                    {
+                        var color = (uint)new Random().Next(0xffffff) | 0x44000000;
+                        context.FillRectangle(
+                            new SolidColorBrush(color),
+                            rect);
+                    }
+
+                    if (DrawFps)
+                    {
+                        RenderFps(context.PlatformImpl, _root.Bounds, true);
+                    }
+                }
+            }
+            catch (RenderTargetCorruptedException ex)
+            {
+                Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
+                _renderTarget.Dispose();
+                _renderTarget = null;
+            }
+        }
+
+        /// <inheritdoc/>
+        public void Resized(Size size)
+        {
+        }
+
+        /// <summary>
+        /// Renders a visual to a render target.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <param name="target">The render target.</param>
+        public static void Render(IVisual visual, IRenderTarget target)
+        {
+            using (var renderer = new ImmediateRenderer(visual))
+            using (var context = new DrawingContext(target.CreateDrawingContext(renderer)))
+            {
+                renderer.Render(context, visual, visual.Bounds);
+            }
+        }
+
+        /// <summary>
+        /// Renders a visual to a drawing context.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <param name="context">The drawing context.</param>
+        public static void Render(IVisual visual, DrawingContext context)
+        {
+            using (var renderer = new ImmediateRenderer(visual))
+            {
+                renderer.Render(context, visual, visual.Bounds);
+            }
+        }
+
+        /// <inheritdoc/>
+        public void AddDirty(IVisual visual)
+        {
+            if (visual.Bounds != Rect.Empty)
+            {
+                var m = visual.TransformToVisual(_root);
+
+                if (m.HasValue)
+                {
+                    var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value);
+                    _renderRoot?.Invalidate(bounds);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Ends the operation of the renderer.
+        /// </summary>
+        public void Dispose()
+        {
+        }
+
+        /// <inheritdoc/>
+        public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
+        {
+            return HitTest(_root, p, filter);
+        }
+
+        /// <inheritdoc/>
+        Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
+        {
+            (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized();
+            return brush.Visual?.Bounds.Size ?? Size.Empty;
+        }
+
+        /// <inheritdoc/>
+        void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
+        {
+            var visual = brush.Visual;
+            Render(new DrawingContext(context), visual, visual.Bounds);
+        }
+
+        private static void ClearTransformedBounds(IVisual visual)
+        {
+            foreach (var e in visual.GetSelfAndVisualDescendents())
+            {
+                BoundsTracker.SetTransformedBounds((Visual)visual, null);
+            }
+        }
+
+        private static Rect GetTransformedBounds(IVisual visual)
+        {
+            if (visual.RenderTransform == null)
+            {
+                return visual.Bounds;
+            }
+            else
+            {
+                var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+                var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin);
+                var m = (-offset) * visual.RenderTransform.Value * (offset);
+                return visual.Bounds.TransformToAABB(m);
+            }
+        }
+
+        static IEnumerable<IVisual> HitTest(
+           IVisual visual,
+           Point p,
+           Func<IVisual, bool> filter)
+        {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
+            if (filter?.Invoke(visual) != false)
+            {
+                bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)visual)?.Contains(p) == true;
+
+                if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0)
+                {
+                    foreach (var child in visual.VisualChildren.SortByZIndex())
+                    {
+                        foreach (var result in HitTest(child, p, filter))
+                        {
+                            yield return result;
+                        }
+                    }
+                }
+
+                if (containsPoint)
+                {
+                    yield return visual;
+                }
+            }
+        }
+
+        private void Render(DrawingContext context, IVisual visual, Rect clipRect)
+        {
+            var opacity = visual.Opacity;
+            var clipToBounds = visual.ClipToBounds;
+            var bounds = new Rect(visual.Bounds.Size);
+
+            if (visual.IsVisible && opacity > 0)
+            {
+                var m = Matrix.CreateTranslation(visual.Bounds.Position);
+
+                var renderTransform = Matrix.Identity;
+
+                if (visual.RenderTransform != null)
+                {
+                    var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+                    var offset = Matrix.CreateTranslation(origin);
+                    renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
+                }
+
+                m = renderTransform * m;
+
+                if (clipToBounds)
+                {
+                    clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
+                }
+
+                using (context.PushPostTransform(m))
+                using (context.PushOpacity(opacity))
+                using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
+                using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
+                using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState))
+                using (context.PushTransformContainer())
+                {
+                    visual.Render(context);
+
+#pragma warning disable 0618
+                    var transformed =
+                        new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
+#pragma warning restore 0618
+
+                    if (visual is Visual)
+                    {
+                        BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
+                    }
+
+                    foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
+                    {
+                        var childBounds = GetTransformedBounds(child);
+
+                        if (!child.ClipToBounds || clipRect.Intersects(childBounds))
+                        {
+                            var childClipRect = clipRect.Translate(-childBounds.Position);
+                            Render(context, child, childClipRect);
+                        }
+                        else
+                        {
+                            ClearTransformedBounds(child);
+                        }
+                    }
+                }
+            }
+
+            if (!visual.IsVisible)
+            {
+                ClearTransformedBounds(visual);
+            }
+        }
+    }
+}

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

@@ -1,67 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Platform;
-using Avalonia.VisualTree;
-
-namespace Avalonia.Rendering
-{
-    public class Renderer : IDisposable, IRenderer
-    {
-        private readonly IRenderLoop _renderLoop;
-        private readonly IRenderRoot _root;
-        private IRenderTarget _renderTarget;
-        private bool _dirty;
-
-        public Renderer(IRenderRoot root, IRenderLoop renderLoop)
-        {
-            Contract.Requires<ArgumentNullException>(root != null);
-
-            _root = root;
-            _renderLoop = renderLoop;
-            _renderLoop.Tick += OnRenderLoopTick;
-        }
-
-        public void AddDirty(IVisual visual)
-        {
-            _dirty = true;
-        }
-
-        public void Dispose()
-        {
-            _renderLoop.Tick -= OnRenderLoopTick;
-        }
-
-        public void Render(Rect rect)
-        {
-            if (_renderTarget == null)
-            {
-                _renderTarget = _root.CreateRenderTarget();
-            }
-
-            try
-            {
-                _renderTarget.Render(_root);
-            }
-            catch (RenderTargetCorruptedException ex)
-            {
-                Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
-                _renderTarget.Dispose();
-                _renderTarget = null;
-            }
-            finally
-            {
-                _dirty = false;
-            }
-        }
-
-        private void OnRenderLoopTick(object sender, EventArgs e)
-        {
-            if (_dirty)
-            {
-                _root.Invalidate(new Rect(_root.ClientSize));
-            }
-        }
-    }
-}

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

@@ -0,0 +1,51 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Media;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering
+{
+    public class RendererBase
+    {
+        private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
+        private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+        private int _framesThisSecond;
+        private int _fps;
+        private FormattedText _fpsText;
+        private TimeSpan _lastFpsUpdate;
+
+        public RendererBase()
+        {
+            _fpsText = new FormattedText
+            {
+                Typeface = new Typeface(null, 18),
+            };
+        }
+
+        protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
+        {
+            var now = _stopwatch.Elapsed;
+            var elapsed = now - _lastFpsUpdate;
+
+            if (incrementFrameCount)
+            {
+                ++_framesThisSecond;
+            }
+
+            if (elapsed.TotalSeconds > 1)
+            {
+                _fps = (int)(_framesThisSecond / elapsed.TotalSeconds);
+                _framesThisSecond = 0;
+                _lastFpsUpdate = now;
+            }
+
+            _fpsText.Text = string.Format("FPS: {0:000}", _fps);
+            var size = _fpsText.Measure();
+            var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
+
+            context.Transform = Matrix.Identity;
+            context.FillRectangle(Brushes.Black, rect);
+            context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl);
+        }
+    }
+}

+ 0 - 205
src/Avalonia.Visuals/Rendering/RendererMixin.cs

@@ -1,205 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.VisualTree;
-
-namespace Avalonia.Rendering
-{
-    /// <summary>
-    /// Extension methods for rendering.
-    /// </summary>
-    /// <remarks>
-    /// This class provides implements the platform-independent parts of <see cref="IRenderTarget"/>.
-    /// </remarks>
-    [SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")]
-    [SuppressMessage("ReSharper", "ForCanBeConvertedToForeach")]
-    public static class RendererMixin
-    {
-        private static int s_frameNum;
-        private static int s_fps;
-        private static int s_currentFrames;
-        private static TimeSpan s_lastMeasure;
-        private static readonly Stopwatch s_stopwatch = Stopwatch.StartNew();
-        private static readonly Stack<List<IVisual>> s_listPool = new Stack<List<IVisual>>();
-        private static readonly ZIndexComparer s_visualComparer = new ZIndexComparer();
-
-        /// <summary>
-        /// Gets or sets a value which determines whether an FPS counted will be drawn on each
-        /// rendered frame.
-        /// </summary>
-        public static bool DrawFpsCounter { get; set; }
-
-        /// <summary>
-        /// Renders the specified visual.
-        /// </summary>
-        /// <param name="renderTarget">IRenderer instance</param>
-        /// <param name="visual">The visual to render.</param>
-        public static void Render(this IRenderTarget renderTarget, IVisual visual)
-        {
-            using (var ctx = renderTarget.CreateDrawingContext())
-            {
-                ctx.Render(visual);
-                s_frameNum++;
-                if (DrawFpsCounter)
-                {
-                    s_currentFrames++;
-                    var now = s_stopwatch.Elapsed;
-                    var elapsed = now - s_lastMeasure;
-                    if (elapsed.TotalSeconds > 1)
-                    {
-                        s_fps = (int) (s_currentFrames/elapsed.TotalSeconds);
-                        s_currentFrames = 0;
-                        s_lastMeasure = now;
-                    }
-                    var pt = new Point(40, 40);
-                    var txt = new FormattedText
-                    {
-                        Text = "Frame #" + s_frameNum + " FPS: " + s_fps,
-                        Typeface = new Typeface("Arial", 18)
-                    };
-                    ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
-                    ctx.DrawText(Brushes.Black, pt, txt);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Renders the specified visual.
-        /// </summary>
-        /// <param name="visual">The visual to render.</param>
-        /// <param name="context">The drawing context.</param>
-        public static void Render(this DrawingContext context, IVisual visual)
-        {
-            context.Render(visual, visual.Bounds);
-        }
-
-        /// <summary>
-        /// Renders the specified visual.
-        /// </summary>
-        /// <param name="visual">The visual to render.</param>
-        /// <param name="context">The drawing context.</param>
-        /// <param name="clipRect">
-        /// The current clip rect, in coordinates relative to <paramref name="visual"/>.
-        /// </param>
-        private static void Render(this DrawingContext context, IVisual visual, Rect clipRect)
-        {
-            var opacity = visual.Opacity;
-            var clipToBounds = visual.ClipToBounds;
-            var bounds = new Rect(visual.Bounds.Size);
-
-            if (visual.IsVisible && opacity > 0)
-            {
-                var m = Matrix.CreateTranslation(visual.Bounds.Position);
-
-                var renderTransform = Matrix.Identity;
-
-                if (visual.RenderTransform != null)
-                {
-                    var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
-                    var offset = Matrix.CreateTranslation(origin);
-                    renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
-                }
-
-                m = renderTransform * m;
-
-                if (clipToBounds)
-                {
-                    clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
-                }
-
-                using (context.PushPostTransform(m))
-                using (context.PushOpacity(opacity))
-                using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
-                using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
-                using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState))
-                using (context.PushTransformContainer())
-                {
-                    visual.Render(context);
-
-#pragma warning disable 0618
-                    var transformed =
-                        new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
-#pragma warning restore 0618
-
-                    if (visual is Visual)
-                    {
-                        BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
-                    }
-
-                    var lst = GetSortedVisualList(visual.VisualChildren);
-
-                    foreach (var child in lst)
-                    {
-                        var childBounds = GetTransformedBounds(child);
-
-                        if (!child.ClipToBounds || clipRect.Intersects(childBounds))
-                        {
-                            var childClipRect = clipRect.Translate(-childBounds.Position);
-                            context.Render(child, childClipRect);
-                        }
-                        else
-                        {
-                            ClearTransformedBounds(child);
-                        }
-                    }
-
-                    ReturnListToPool(lst);
-                }
-            }
-            
-            if (!visual.IsVisible)
-            {
-                ClearTransformedBounds(visual);
-            }
-        }
-
-        private static void ClearTransformedBounds(IVisual visual)
-        {
-            foreach (var e in visual.GetSelfAndVisualDescendents())
-            {
-                BoundsTracker.SetTransformedBounds((Visual)visual, null);
-            }
-        }
-
-        private static void ReturnListToPool(List<IVisual> lst)
-        {
-            lst.Clear();
-            s_listPool.Push(lst);
-        }
-
-        private static List<IVisual> GetSortedVisualList(IReadOnlyList<IVisual> source)
-        {
-            var lst = s_listPool.Count == 0 ? new List<IVisual>() : s_listPool.Pop();
-            for (var c = 0; c < source.Count; c++)
-                lst.Add(source[c]);
-            lst.Sort(s_visualComparer);
-            return lst;
-        }
-
-        private static Rect GetTransformedBounds(IVisual visual)
-        {
-            if (visual.RenderTransform == null)
-            {
-                return visual.Bounds;
-            }
-            else
-            {
-                var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
-                var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin);
-                var m = (-offset) * visual.RenderTransform.Value * (offset);
-                return visual.Bounds.TransformToAABB(m);
-            }
-        }
-
-        class ZIndexComparer : IComparer<IVisual>
-        {
-            public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex);
-        }
-    }
-}

+ 193 - 0
src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs

@@ -0,0 +1,193 @@
+// 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 Avalonia.Media;
+
+namespace Avalonia.Rendering.Utilities
+{
+    public class TileBrushCalculator
+    {
+        private readonly Size _imageSize;
+        private readonly Rect _drawRect;
+
+        public bool IsValid { get; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TileBrushCalculator"/> class.
+        /// </summary>
+        /// <param name="brush">The brush to be rendered.</param>
+        /// <param name="contentSize">The size of the content of the tile brush.</param>
+        /// <param name="targetSize">The size of the control to which the brush is being rendered.</param>
+        public TileBrushCalculator(ITileBrush brush, Size contentSize, Size targetSize)
+            : this(
+                  brush.TileMode,
+                  brush.Stretch,
+                  brush.AlignmentX,
+                  brush.AlignmentY,
+                  brush.SourceRect,
+                  brush.DestinationRect,
+                  contentSize,
+                  targetSize)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TileBrushCalculator"/> class.
+        /// </summary>
+        /// <param name="tileMode">The brush's tile mode.</param>
+        /// <param name="stretch">The brush's stretch.</param>
+        /// <param name="alignmentX">The brush's horizontal alignment.</param>
+        /// <param name="alignmentY">The brush's vertical alignment.</param>
+        /// <param name="sourceRect">The brush's source rect</param>
+        /// <param name="destinationRect">The brush's destination rect.</param>
+        /// <param name="contentSize">The size of the content of the tile brush.</param>
+        /// <param name="targetSize">The size of the control to which the brush is being rendered.</param>
+        public TileBrushCalculator(
+            TileMode tileMode,
+            Stretch stretch,
+            AlignmentX alignmentX,
+            AlignmentY alignmentY,
+            RelativeRect sourceRect,
+            RelativeRect destinationRect,
+            Size contentSize,
+            Size targetSize)
+        {
+            _imageSize = contentSize;
+
+            SourceRect = sourceRect.ToPixels(_imageSize);
+            DestinationRect = destinationRect.ToPixels(targetSize);
+
+            var scale = stretch.CalculateScaling(DestinationRect.Size, SourceRect.Size);
+            var translate = CalculateTranslate(alignmentX, alignmentY, SourceRect, DestinationRect, scale);
+
+            IntermediateSize = tileMode == TileMode.None ? targetSize : DestinationRect.Size;
+            IntermediateTransform = CalculateIntermediateTransform(
+                tileMode,
+                SourceRect,
+                DestinationRect,
+                scale,
+                translate,
+                out _drawRect);
+        }
+
+        /// <summary>
+        /// Gets the rectangle on the destination control to which content should be rendered.
+        /// </summary>
+        /// <remarks>
+        /// If <see cref="TileMode"/> of the brush is repeating then this is describes rectangle
+        /// of a single repeat of the tiled content.
+        /// </remarks>
+        public Rect DestinationRect { get; }
+
+        /// <summary>
+        /// Gets the clip rectangle on the intermediate image with which the brush content should be
+        /// drawn when <see cref="NeedsIntermediate"/> is true.
+        /// </summary>
+        public Rect IntermediateClip => _drawRect;
+
+        /// <summary>
+        /// Gets the size of the intermediate image that should be created when
+        /// <see cref="NeedsIntermediate"/> is true.
+        /// </summary>
+        public Size IntermediateSize { get; }
+
+        /// <summary>
+        /// Gets the transform to be used when rendering to the intermediate image when
+        /// <see cref="NeedsIntermediate"/> is true.
+        /// </summary>
+        public Matrix IntermediateTransform { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether an intermediate image should be created in order to
+        /// render the tile brush.
+        /// </summary>
+        /// <remarks>
+        /// Intermediate images are required when a brush's <see cref="TileMode"/> is not repeating
+        /// but the source and destination aspect ratios are unequal, as all of the currently
+        /// supported rendering backends do not support non-tiled image brushes.
+        /// </remarks>
+        public bool NeedsIntermediate
+        {
+            get
+            {
+                if (IntermediateTransform != Matrix.Identity)
+                    return true;
+                if (SourceRect.Position != default(Point))
+                    return true;
+                if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio)
+                    return false;
+                if ((int)SourceRect.Width != _imageSize.Width ||
+                    (int)SourceRect.Height != _imageSize.Height)
+                    return true;
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Gets the area of the source content to be rendered.
+        /// </summary>
+        public Rect SourceRect { get; }
+
+        public static Vector CalculateTranslate(
+            AlignmentX alignmentX,
+            AlignmentY alignmentY,
+            Rect sourceRect,
+            Rect destinationRect,
+            Vector scale)
+        {
+            var x = 0.0;
+            var y = 0.0;
+            var size = sourceRect.Size * scale;
+
+            switch (alignmentX)
+            {
+                case AlignmentX.Center:
+                    x += (destinationRect.Width - size.Width) / 2;
+                    break;
+                case AlignmentX.Right:
+                    x += destinationRect.Width - size.Width;
+                    break;
+            }
+
+            switch (alignmentY)
+            {
+                case AlignmentY.Center:
+                    y += (destinationRect.Height - size.Height) / 2;
+                    break;
+                case AlignmentY.Bottom:
+                    y += destinationRect.Height - size.Height;
+                    break;
+            }
+
+            return new Vector(x, y);
+        }
+
+        public static Matrix CalculateIntermediateTransform(
+            TileMode tileMode,
+            Rect sourceRect,
+            Rect destinationRect,
+            Vector scale,
+            Vector translate,
+            out Rect drawRect)
+        {
+            var transform = Matrix.CreateTranslation(-sourceRect.Position) *
+                            Matrix.CreateScale(scale) *
+                            Matrix.CreateTranslation(translate);
+            Rect dr;
+
+            if (tileMode == TileMode.None)
+            {
+                dr = destinationRect;
+                transform *= Matrix.CreateTranslation(destinationRect.Position);
+            }
+            else
+            {
+                dr = new Rect(destinationRect.Size);
+            }
+
+            drawRect = dr;
+
+            return transform;
+        }
+    }
+}

+ 13 - 0
src/Avalonia.Visuals/Rendering/ZIndexComparer.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class ZIndexComparer : IComparer<IVisual>
+    {
+        public static readonly ZIndexComparer Instance = new ZIndexComparer();
+
+        public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex);
+    }
+}

+ 2 - 2
src/Avalonia.Visuals/Visual.cs

@@ -498,14 +498,14 @@ namespace Avalonia
 
             if (VisualRoot != null)
             {
-                var e = new VisualTreeAttachmentEventArgs(VisualRoot);
+                var e = new VisualTreeAttachmentEventArgs(old, VisualRoot);
                 OnDetachedFromVisualTreeCore(e);
             }
 
             if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
             {
                 var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
-                var e = new VisualTreeAttachmentEventArgs(root);
+                var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
                 OnAttachedToVisualTreeCore(e);
             }
 

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

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

+ 0 - 2
src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj

@@ -53,7 +53,6 @@
     <Compile Include="Media\Imaging\BitmapImpl.cs" />
     <Compile Include="Media\Imaging\RenderTargetBitmapImpl.cs" />
     <Compile Include="Media\RadialGradientBrushImpl.cs" />
-    <Compile Include="Media\TileBrushes.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="RenderTarget.cs" />
     <Compile Include="CairoExtensions.cs" />
@@ -63,7 +62,6 @@
     <Compile Include="Media\SolidColorBrushImpl.cs" />
     <Compile Include="Media\LinearGradientBrushImpl.cs" />
     <Compile Include="Media\ImageBrushImpl.cs" />
-    <Compile Include="Media\VisualBrushImpl.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\Avalonia.Animation\Avalonia.Animation.csproj">

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

@@ -68,7 +68,7 @@ namespace Avalonia.Cairo
                 "Don't know how to create a Cairo renderer from any of the provided surfaces."));
         }
 
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height, double dpiX, double dpiY)
         {
             return new RenderTargetBitmapImpl(new ImageSurface(Format.Argb32, width, height));
         }

+ 77 - 49
src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs

@@ -8,6 +8,7 @@ using System.Reactive.Disposables;
 using Avalonia.Cairo.Media.Imaging;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 
 namespace Avalonia.Cairo.Media
 {
@@ -18,32 +19,30 @@ namespace Avalonia.Cairo.Media
     /// </summary>
     public class DrawingContext : IDrawingContextImpl, IDisposable
     {
-        /// <summary>
-        /// The cairo context.
-        /// </summary>
         private readonly Cairo.Context _context;
-
+        private readonly IVisualBrushRenderer _visualBrushRenderer;
         private readonly Stack<BrushImpl> _maskStack = new Stack<BrushImpl>();
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DrawingContext"/> class.
         /// </summary>
         /// <param name="surface">The target surface.</param>
-        public DrawingContext(Cairo.Surface surface)
+        public DrawingContext(Cairo.Surface surface, IVisualBrushRenderer visualBrushRenderer)
         {
             _context = new Cairo.Context(surface);
+            _visualBrushRenderer = visualBrushRenderer;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DrawingContext"/> class.
         /// </summary>
         /// <param name="surface">The GDK drawable.</param>
-        public DrawingContext(Gdk.Drawable drawable)
+        public DrawingContext(Gdk.Drawable drawable, IVisualBrushRenderer visualBrushRenderer)
         {
             _context = Gdk.CairoHelper.Create(drawable);
+            _visualBrushRenderer = visualBrushRenderer;
         }
 
-
         private Matrix _transform = Matrix.Identity;
         /// <summary>
         /// Gets the current transform of the drawing context.
@@ -55,7 +54,7 @@ namespace Avalonia.Cairo.Media
             {
                 _transform = value;
                 _context.Matrix = value.ToCairo();
-                
+
             }
         }
 
@@ -82,42 +81,47 @@ namespace Avalonia.Cairo.Media
         /// <param name="destRect">The rect in the output to draw to.</param>
         public void DrawImage(IBitmapImpl bitmap, double opacity, Rect sourceRect, Rect destRect)
         {
-            var impl = bitmap as BitmapImpl;
-            var size = new Size(impl.PixelWidth, impl.PixelHeight);
+            var pixbuf = bitmap as Gdk.Pixbuf;
+            var rtb = bitmap as RenderTargetBitmapImpl;
+            var size = new Size(pixbuf?.Width ?? rtb.PixelWidth, pixbuf?.Height ?? rtb.PixelHeight);
             var scale = new Vector(destRect.Width / sourceRect.Width, destRect.Height / sourceRect.Height);
 
             _context.Save();
             _context.Scale(scale.X, scale.Y);
             destRect /= scale;
 
-			if (opacityOverride < 1.0f) {
-				_context.PushGroup ();
-				Gdk.CairoHelper.SetSourcePixbuf (
-					_context, 
-					impl, 
-					-sourceRect.X + destRect.X, 
-					-sourceRect.Y + destRect.Y);
-
-				_context.Rectangle (destRect.ToCairo ());
-				_context.Fill ();
-				_context.PopGroupToSource ();
-				_context.PaintWithAlpha (opacityOverride);
-			} else {
-				_context.PushGroup ();
-				Gdk.CairoHelper.SetSourcePixbuf (
-					_context, 
-					impl, 
-					-sourceRect.X + destRect.X, 
-					-sourceRect.Y + destRect.Y);
-
-                _context.Rectangle (destRect.ToCairo ());
-                _context.Fill ();
-                _context.PopGroupToSource ();
-                _context.PaintWithAlpha (opacityOverride);			
+            _context.PushGroup();
+
+            if (pixbuf != null)
+            {
+                Gdk.CairoHelper.SetSourcePixbuf(
+                    _context,
+                    pixbuf,
+                    -sourceRect.X + destRect.X,
+                    -sourceRect.Y + destRect.Y);
+            }
+            else
+            {
+                _context.SetSourceSurface(
+                        rtb.Surface,
+                        (int)(-sourceRect.X + destRect.X),
+                        (int)(-sourceRect.Y + destRect.Y));
             }
+
+            _context.Rectangle(destRect.ToCairo());
+            _context.Fill();
+            _context.PopGroupToSource();
+            _context.PaintWithAlpha(opacityOverride);
             _context.Restore();
         }
 
+        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();
+        }
+
         /// <summary>
         /// Draws a line.
         /// </summary>
@@ -127,8 +131,8 @@ namespace Avalonia.Cairo.Media
         public void DrawLine(Pen pen, Point p1, Point p2)
         {
             var size = new Rect(p1, p2).Size;
-            
-            using (var p = SetPen(pen, size)) 
+
+            using (var p = SetPen(pen, size))
             {
                 _context.MoveTo(p1.ToCairo());
                 _context.LineTo(p2.ToCairo());
@@ -149,7 +153,7 @@ namespace Avalonia.Cairo.Media
             var oldMatrix = Transform;
             Transform = impl.Transform * Transform;
 
-            
+
             if (brush != null)
             {
                 _context.AppendPath(impl.Path);
@@ -184,9 +188,9 @@ namespace Avalonia.Cairo.Media
         /// <param name="rect">The rectangle bounds.</param>
         public void DrawRectangle(Pen pen, Rect rect, float cornerRadius)
         {
-            using (var p = SetPen(pen, rect.Size)) 
+            using (var p = SetPen(pen, rect.Size))
             {
-                _context.Rectangle(rect.ToCairo ());
+                _context.Rectangle(rect.ToCairo());
                 _context.Stroke();
             }
         }
@@ -202,7 +206,7 @@ namespace Avalonia.Cairo.Media
             var layout = ((FormattedTextImpl)text).Layout;
             _context.MoveTo(origin.X, origin.Y);
 
-            using (var b = SetBrush(foreground, new Size(0, 0))) 
+            using (var b = SetBrush(foreground, new Size(0, 0)))
             {
                 Pango.CairoHelper.ShowLayout(_context, layout);
             }
@@ -215,9 +219,9 @@ namespace Avalonia.Cairo.Media
         /// <param name="rect">The rectangle bounds.</param>
         public void FillRectangle(IBrush brush, Rect rect, float cornerRadius)
         {
-            using (var b = SetBrush(brush, rect.Size)) 
+            using (var b = SetBrush(brush, rect.Size))
             {
-                _context.Rectangle(rect.ToCairo ());
+                _context.Rectangle(rect.ToCairo());
                 _context.Fill();
             }
         }
@@ -272,10 +276,10 @@ namespace Avalonia.Cairo.Media
 
             return Disposable.Create(() =>
             {
-               _context.Restore();
+                _context.Restore();
             });
         }
-        
+
         private double opacityOverride = 1.0f;
 
         private IDisposable SetBrush(IBrush brush, Size destinationSize)
@@ -315,11 +319,35 @@ namespace Avalonia.Cairo.Media
             }
             else if (imageBrush != null)
             {
-                impl = new ImageBrushImpl(imageBrush, destinationSize);
+                impl = new ImageBrushImpl(imageBrush, (BitmapImpl)imageBrush.Source.PlatformImpl, destinationSize);
             }
             else if (visualBrush != null)
             {
-                impl = new VisualBrushImpl(visualBrush, destinationSize);
+                if (_visualBrushRenderer != null)
+                {
+                    var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+
+                    if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
+                    {
+                        using (var intermediate = new Cairo.ImageSurface(Cairo.Format.ARGB32, (int)intermediateSize.Width, (int)intermediateSize.Height))
+                        {
+                            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer))
+                            {
+                                ctx.Clear(Colors.Transparent);
+                                _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
+                            }
+
+                            return new ImageBrushImpl(
+                                visualBrush,
+                                new RenderTargetBitmapImpl(intermediate),
+                                destinationSize);
+                        }
+                    }
+                }
+                else
+                {
+                    throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+                }
             }
             else
             {
@@ -351,7 +379,7 @@ namespace Avalonia.Cairo.Media
 
             if (pen.Brush == null)
                 return Disposable.Empty;
-            
+
             return SetBrush(pen.Brush, destinationSize);
         }
 
@@ -377,10 +405,10 @@ namespace Avalonia.Cairo.Media
         public void PopOpacityMask()
         {
             _context.PopGroupToSource();
-			var brushImpl = _maskStack.Pop ();
+            var brushImpl = _maskStack.Pop();
 
             _context.Mask(brushImpl.PlatformBrush);
-			brushImpl.Dispose ();
+            brushImpl.Dispose();
         }
     }
 }

+ 49 - 8
src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs

@@ -1,15 +1,56 @@
-using System;
 using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.Utilities;
 using global::Cairo;
 
 namespace Avalonia.Cairo.Media
 {
-	public class ImageBrushImpl : BrushImpl
-	{
-		public ImageBrushImpl(IImageBrush brush, Size destinationSize)
-		{
-			this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize);
-		}
-	}
+    public class ImageBrushImpl : BrushImpl
+    {
+        public ImageBrushImpl(
+            ITileBrush brush,
+            IBitmapImpl bitmap,
+            Size targetSize)
+        {
+            var calc = new TileBrushCalculator(brush, new Size(bitmap.PixelWidth, bitmap.PixelHeight), targetSize);
+
+            using (var intermediate = new ImageSurface(Format.ARGB32, (int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height))
+            {
+                using (var context = new RenderTarget(intermediate).CreateDrawingContext(null))
+                {
+                    var rect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
+
+                    context.Clear(Colors.Transparent);
+                    context.PushClip(calc.IntermediateClip);
+                    context.Transform = calc.IntermediateTransform;
+                    context.DrawImage(bitmap, 1, rect, rect);
+                    context.PopClip();
+                }
+
+                var result = new SurfacePattern(intermediate);
+
+                if ((brush.TileMode & TileMode.FlipXY) != 0)
+                {
+                    // TODO: Currently always FlipXY as that's all cairo supports natively. 
+                    // Support separate FlipX and FlipY by drawing flipped images to intermediate
+                    // surface.
+                    result.Extend = Extend.Reflect;
+                }
+                else
+                {
+                    result.Extend = Extend.Repeat;
+                }
+
+                if (brush.TileMode != TileMode.None)
+                {
+                    var matrix = result.Matrix;
+                    matrix.InitTranslate(-calc.DestinationRect.X, -calc.DestinationRect.Y);
+                    result.Matrix = matrix;
+                }
+
+                PlatformBrush = result;
+            }
+        }
+    }
 }
 

+ 2 - 2
src/Gtk/Avalonia.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs

@@ -40,9 +40,9 @@ namespace Avalonia.Cairo.Media.Imaging
             Surface.WriteToPng(fileName);
         }
 
-        public Avalonia.Media.DrawingContext CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
-            return _renderTarget.CreateDrawingContext();
+            return _renderTarget.CreateDrawingContext(visualBrushRenderer);
         }
 
         public void Save(Stream stream)

+ 0 - 55
src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs

@@ -1,55 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Cairo;
-using Avalonia.Cairo.Media.Imaging;
-using Avalonia.Layout;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.RenderHelpers;
-
-namespace Avalonia.Cairo.Media
-{
-    internal static class TileBrushes
-    {
-        public static SurfacePattern CreateTileBrush(ITileBrush brush, Size targetSize)
-        {
-            var helper = new TileBrushImplHelper(brush, targetSize);
-            if (!helper.IsValid)
-                return null;
-            
-			using (var intermediate = new ImageSurface(Format.ARGB32, (int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height))
-            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
-            {
-                helper.DrawIntermediate(ctx);
-
-                var result = new SurfacePattern(intermediate);
-
-                if ((brush.TileMode & TileMode.FlipXY) != 0)
-                {
-                    // TODO: Currently always FlipXY as that's all cairo supports natively. 
-                    // Support separate FlipX and FlipY by drawing flipped images to intermediate
-                    // surface.
-                    result.Extend = Extend.Reflect;
-                }
-                else
-                {
-                    result.Extend = Extend.Repeat;
-                }
-
-                if (brush.TileMode != TileMode.None)
-                {
-                    var matrix = result.Matrix;
-                    matrix.InitTranslate(-helper.DestinationRect.X, -helper.DestinationRect.Y);
-                    result.Matrix = matrix;
-                }
-
-                return result;
-            }
-        }
-
-      
-    }
-}

+ 0 - 15
src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs

@@ -1,15 +0,0 @@
-using System;
-using Avalonia.Media;
-using global::Cairo;
-
-namespace Avalonia.Cairo.Media
-{
-	public class VisualBrushImpl : BrushImpl
-	{
-		public VisualBrushImpl(IVisualBrush brush, Size destinationSize)
-		{
-			this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize);
-		}
-	}
-}
-

+ 4 - 5
src/Gtk/Avalonia.Cairo/RenderTarget.cs

@@ -42,15 +42,14 @@ namespace Avalonia.Cairo
         /// <summary>
         /// Creates a cairo surface that targets a platform-specific resource.
         /// </summary>
+        /// <param name="visualBrushRenderer">The visual brush renderer to use.</param>
         /// <returns>A surface wrapped in an <see cref="Avalonia.Media.DrawingContext"/>.</returns>
-        public DrawingContext CreateDrawingContext() => new DrawingContext(CreateMediaDrawingContext());
-
-        public IDrawingContextImpl CreateMediaDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
             if (_drawableAccessor != null)
-                return new Media.DrawingContext(_drawableAccessor());
+                return new Media.DrawingContext(_drawableAccessor(), visualBrushRenderer);
             if (_surface != null)
-                return new Media.DrawingContext(_surface);
+                return new Media.DrawingContext(_surface, visualBrushRenderer);
             throw new InvalidOperationException("Unspecified render target");
         }
 

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

@@ -115,7 +115,7 @@ namespace Avalonia.Gtk
 
         public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
         {
-            return new Renderer(root, renderLoop);
+            return new ImmediateRenderer(root);
         }
 
         public IWindowIconImpl LoadIcon(string fileName)

+ 5 - 2
src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs

@@ -15,7 +15,7 @@ using Avalonia.Gtk3;
 
 namespace Avalonia.Gtk3
 {
-    public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
+    public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface, IRendererFactory
     {
         internal static readonly Gtk3Platform Instance = new Gtk3Platform();
         internal static readonly MouseDevice Mouse = new MouseDevice();
@@ -52,7 +52,10 @@ 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);
 

+ 1 - 2
src/Shared/RenderHelpers/RenderHelpers.projitems

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
@@ -11,6 +11,5 @@
   <ItemGroup>
     <Compile Include="$(MSBuildThisFileDirectory)ArcToHelper.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)QuadBezierHelper.cs" />
-    <Compile Include="$(MSBuildThisFileDirectory)TileBrushImplHelper.cs" />
   </ItemGroup>
 </Project>

+ 0 - 209
src/Shared/RenderHelpers/TileBrushImplHelper.cs

@@ -1,209 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Avalonia.Controls;
-using Avalonia.Media;
-using Avalonia.Rendering;
-using Avalonia.VisualTree;
-
-namespace Avalonia.RenderHelpers
-{
-    internal class TileBrushImplHelper
-    {
-        public Size IntermediateSize { get; }
-        public Rect DestinationRect { get; }
-        private readonly TileMode _tileMode;
-        private readonly Rect _sourceRect;
-        private readonly Vector _scale;
-        private readonly Vector _translate;
-        private readonly Size _imageSize;
-        private readonly IVisualBrush _visualBrush;
-        private readonly IImageBrush _imageBrush;
-        private readonly Matrix _transform;
-        private readonly Rect _drawRect;
-
-        public bool IsValid { get; }
-        
-        public TileBrushImplHelper(ITileBrush brush, Size targetSize)
-        {
-            _imageBrush = brush as IImageBrush;
-            _visualBrush = brush as IVisualBrush;
-            if (_imageBrush != null)
-            {
-                if (_imageBrush.Source == null)
-                    return;
-                _imageSize = new Size(_imageBrush.Source.PixelWidth, _imageBrush.Source.PixelHeight);
-                IsValid = true;
-            }
-            else if (_visualBrush != null)
-            {
-                var control = _visualBrush.Visual as IControl;
-
-                if (control != null)
-                {
-                    EnsureInitialized(control);
-
-                    if (control.IsArrangeValid == false)
-                    {
-                        control.Measure(Size.Infinity);
-                        control.Arrange(new Rect(control.DesiredSize));
-                    }
-
-                    _imageSize = control.Bounds.Size;
-                    IsValid = true;
-                }
-            }
-            else
-                return;
-
-            _tileMode = brush.TileMode;
-            _sourceRect = brush.SourceRect.ToPixels(_imageSize);
-            DestinationRect = brush.DestinationRect.ToPixels(targetSize);
-            _scale = brush.Stretch.CalculateScaling(DestinationRect.Size, _sourceRect.Size);
-            _translate = CalculateTranslate(brush, _sourceRect, DestinationRect, _scale);
-            IntermediateSize = CalculateIntermediateSize(_tileMode, targetSize, DestinationRect.Size);
-            _transform = CalculateIntermediateTransform(
-                _tileMode,
-                _sourceRect,
-                DestinationRect,
-                _scale,
-                _translate,
-                out _drawRect);
-        }
-
-        public bool NeedsIntermediateSurface
-        {
-            get
-            {
-                if (_imageBrush == null)
-                    return true;
-                if (_transform != Matrix.Identity)
-                    return true;
-                if (_sourceRect.Position != default(Point))
-                    return true;
-                if ((int) _sourceRect.Width != _imageBrush.Source.PixelWidth ||
-                    (int) _sourceRect.Height != _imageBrush.Source.PixelHeight)
-                    return true;
-                return false;
-            }
-        }
-
-        public T GetDirect<T>() => (T) _imageBrush?.Source.PlatformImpl;
-
-        public void DrawIntermediate(DrawingContext ctx)
-        {
-            using (ctx.PushClip(_drawRect))
-            using (ctx.PushPostTransform(_transform))
-            {
-                if (_imageBrush != null)
-                {
-                    var bmpRc = new Rect(0, 0, _imageBrush.Source.PixelWidth, _imageBrush.Source.PixelHeight);
-                    ctx.DrawImage(_imageBrush.Source, 1, bmpRc, bmpRc);
-                }
-                else if (_visualBrush != null)
-                {
-                    using (ctx.PushPostTransform(Matrix.CreateTranslation(-_visualBrush.Visual.Bounds.Position)))
-                    {
-                        ctx.Render(_visualBrush.Visual);
-                    }
-                }
-            }
-        }
-
-
-        /// <summary>
-        /// Calculates a translate based on an <see cref="ITileBrush"/>, a source and destination
-        /// rectangle and a scale.
-        /// </summary>
-        /// <param name="brush">The brush.</param>
-        /// <param name="sourceRect">The source rectangle.</param>
-        /// <param name="destinationRect">The destination rectangle.</param>
-        /// <param name="scale">The _scale factor.</param>
-        /// <returns>A vector with the X and Y _translate.</returns>
-
-        public static Vector CalculateTranslate(
-            ITileBrush brush,
-            Rect sourceRect,
-            Rect destinationRect,
-            Vector scale)
-        {
-            var x = 0.0;
-            var y = 0.0;
-            var size = sourceRect.Size*scale;
-
-            switch (brush.AlignmentX)
-            {
-                case AlignmentX.Center:
-                    x += (destinationRect.Width - size.Width)/2;
-                    break;
-                case AlignmentX.Right:
-                    x += destinationRect.Width - size.Width;
-                    break;
-            }
-
-            switch (brush.AlignmentY)
-            {
-                case AlignmentY.Center:
-                    y += (destinationRect.Height - size.Height)/2;
-                    break;
-                case AlignmentY.Bottom:
-                    y += destinationRect.Height - size.Height;
-                    break;
-            }
-
-            return new Vector(x, y);
-        }
-
-        public static Matrix CalculateIntermediateTransform(
-            TileMode tileMode,
-            Rect sourceRect,
-            Rect destinationRect,
-            Vector scale,
-            Vector translate,
-            out Rect drawRect)
-        {
-            var transform = Matrix.CreateTranslation(-sourceRect.Position)*
-                            Matrix.CreateScale(scale)*
-                            Matrix.CreateTranslation(translate);
-            Rect dr;
-
-            if (tileMode == TileMode.None)
-            {
-                dr = destinationRect;
-                transform *= Matrix.CreateTranslation(destinationRect.Position);
-            }
-            else
-            {
-                dr = new Rect(destinationRect.Size);
-            }
-
-            drawRect = dr;
-
-            return transform;
-        }
-
-        private static Size CalculateIntermediateSize(
-            TileMode tileMode,
-            Size targetSize,
-            Size destinationSize) => tileMode == TileMode.None ? targetSize : destinationSize;
-
-        private static void EnsureInitialized(IControl control)
-        {
-            foreach (var i in control.GetSelfAndVisualDescendents())
-            {
-                var c = i as IControl;
-
-                if (c?.IsInitialized == false)
-                {
-                    var init = c as ISupportInitialize;
-
-                    if (init != null)
-                    {
-                        init.BeginInit();
-                        init.EndInit();
-                    }
-                }
-            }
-        }
-    }
-}

+ 8 - 6
src/Skia/Avalonia.Skia/BitmapImpl.cs

@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
 using System.Text;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using SkiaSharp;
 
 namespace Avalonia.Skia
@@ -66,9 +67,10 @@ namespace Avalonia.Skia
         {
             private readonly SKSurface _surface;
 
-            public BitmapDrawingContext(SKBitmap bitmap) : this(CreateSurface(bitmap))
+            public BitmapDrawingContext(SKBitmap bitmap, IVisualBrushRenderer visualBrushRenderer)
+                : this(CreateSurface(bitmap), visualBrushRenderer)
             {
-                
+
             }
 
             private static SKSurface CreateSurface(SKBitmap bitmap)
@@ -80,7 +82,8 @@ namespace Avalonia.Skia
                 return rv;
             }
 
-            public BitmapDrawingContext(SKSurface surface) : base(surface.Canvas)
+            public BitmapDrawingContext(SKSurface surface, IVisualBrushRenderer visualBrushRenderer)
+                : base(surface.Canvas, visualBrushRenderer)
             {
                 _surface = surface;
             }
@@ -92,10 +95,9 @@ namespace Avalonia.Skia
             }
         }
 
-        public DrawingContext CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
-
-            return new DrawingContext(new BitmapDrawingContext(Bitmap));
+            return new BitmapDrawingContext(Bitmap, visualBrushRenderer);
         }
 
         public void Save(Stream stream)

+ 57 - 10
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -1,11 +1,11 @@
 using Avalonia.Media;
-using Avalonia.Media.Imaging;
-using Avalonia.RenderHelpers;
 using SkiaSharp;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Rendering.Utilities;
 
 namespace Avalonia.Skia
 {
@@ -13,17 +13,22 @@ namespace Avalonia.Skia
     {
         private readonly Matrix? _postTransform;
         private readonly IDisposable[] _disposables;
+        private readonly IVisualBrushRenderer _visualBrushRenderer;
         private Stack<PaintWrapper> maskStack = new Stack<PaintWrapper>();
         
         public SKCanvas Canvas { get; private set; }
 
-        public DrawingContextImpl(SKCanvas canvas, Matrix? postTransform = null, params IDisposable[] disposables)
+        public DrawingContextImpl(
+            SKCanvas canvas,
+            IVisualBrushRenderer visualBrushRenderer,
+            Matrix? postTransform = null,
+            params IDisposable[] disposables)
         {
             if (postTransform.HasValue && !postTransform.Value.IsIdentity)
                 _postTransform = postTransform;
+            _visualBrushRenderer = visualBrushRenderer;
             _disposables = disposables;
             Canvas = canvas;
-            Canvas.Clear();
             Transform = Matrix.Identity;
         }
 
@@ -194,14 +199,56 @@ namespace Avalonia.Skia
             }
 
             var tileBrush = brush as ITileBrush;
-            if (tileBrush != null)
+            var visualBrush = brush as IVisualBrush;
+            var tileBrushImage = default(BitmapImpl);
+
+            if (visualBrush != null)
             {
-                var helper = new TileBrushImplHelper(tileBrush, targetSize);
-                var bitmap = new BitmapImpl((int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height);
+                if (_visualBrushRenderer != null)
+                {
+                    var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+
+                    if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
+                    {
+                        var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height);
+
+                        using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
+                        {
+                            ctx.Clear(Colors.Transparent);
+                            _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
+                        }
+
+                        rv.AddDisposable(tileBrushImage);
+                        tileBrushImage = intermediate;
+                    }
+                }
+                else
+                {
+                    throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+                }
+            }
+            else
+            {
+                tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl);
+            }
+
+            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);
                 rv.AddDisposable(bitmap);
-                using (var ctx = bitmap.CreateDrawingContext())
-                    helper.DrawIntermediate(ctx);
-                SKMatrix translation = SKMatrix.MakeTranslation(-(float)helper.DestinationRect.X, -(float)helper.DestinationRect.Y);
+                using (var context = bitmap.CreateDrawingContext(null))
+                {
+                    var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
+
+                    context.Clear(Colors.Transparent);
+                    context.PushClip(calc.IntermediateClip);
+                    context.Transform = calc.IntermediateTransform;
+                    context.DrawImage(tileBrushImage, 1, rect, rect);
+                    context.PopClip();
+                }
+
+                SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y);
                 SKShaderTileMode tileX =
                     tileBrush.TileMode == TileMode.None
                         ? SKShaderTileMode.Clamp

+ 6 - 6
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -4,6 +4,7 @@ using System.Text;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using SkiaSharp;
 
 namespace Avalonia.Skia
@@ -56,7 +57,7 @@ namespace Avalonia.Skia
             
         }
 
-        public DrawingContext CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
             var fb = _surface.Lock();
             PixelFormatShim shim = null;
@@ -69,15 +70,14 @@ namespace Avalonia.Skia
                 throw new Exception("Unable to create a surface for pixel format " + fb.Format +
                                     " or pixel format translator");
             var canvas = surface.Canvas;
-            
-            
-            
+
+
+
             canvas.RestoreToCount(0);
             canvas.Save();
-            canvas.Clear(SKColors.Red);
             canvas.ResetMatrix();
             var scale = Matrix.CreateScale(fb.Dpi.Width / 96, fb.Dpi.Height / 96);
-            return new DrawingContext(new DrawingContextImpl(canvas, scale, canvas, surface, shim, fb));
+            return new DrawingContextImpl(canvas, visualBrushRenderer, scale, canvas, surface, shim, fb);
         }
     }
 }

+ 7 - 9
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -5,16 +5,15 @@ using System.Linq;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Media;
 using Avalonia.Platform;
-using Avalonia.Rendering;
 using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    public partial class PlatformRenderInterface : IPlatformRenderInterface, IRendererFactory
+    public partial class PlatformRenderInterface : IPlatformRenderInterface
     {
         public IBitmapImpl CreateBitmap(int width, int height)
         {
-            return CreateRenderTargetBitmap(width, height);
+            return CreateRenderTargetBitmap(width, height, 96, 96);
         }
 
         public IFormattedTextImpl CreateFormattedText(
@@ -67,12 +66,11 @@ namespace Avalonia.Skia
             }
         }
 
-        public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
-        {
-            return new Renderer(root, renderLoop);
-        }
-
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
+            int width,
+            int height,
+            double dpiX,
+            double dpiY)
         {
             if (width < 1)
                 throw new ArgumentException("Width can't be less than 1", nameof(width));

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

@@ -27,8 +27,7 @@ namespace Avalonia.Skia
         {
             var renderInterface = new PlatformRenderInterface();
             AvaloniaLocator.CurrentMutable
-                .Bind<IPlatformRenderInterface>().ToConstant(renderInterface)
-                .Bind<IRendererFactory>().ToConstant(renderInterface);
+                .Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
         }
 
         public static bool ForceSoftwareRendering

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

@@ -54,6 +54,7 @@
     <Compile Include="Media\BrushImpl.cs" />
     <Compile Include="Media\BrushWrapper.cs" />
     <Compile Include="Media\DrawingContextImpl.cs" />
+    <Compile Include="Media\ImageBrushImpl.cs" />
     <Compile Include="Media\Imaging\BitmapImpl.cs" />
     <Compile Include="Media\Imaging\D2DBitmapImpl.cs" />
     <Compile Include="Media\Imaging\RenderTargetBitmapImpl.cs" />
@@ -62,7 +63,6 @@
     <Compile Include="Media\RadialGradientBrushImpl.cs" />
     <Compile Include="Media\LinearGradientBrushImpl.cs" />
     <Compile Include="Media\AvaloniaTextRenderer.cs" />
-    <Compile Include="Media\TileBrushImpl.cs" />
     <Compile Include="Media\SolidColorBrushImpl.cs" />
     <Compile Include="Media\StreamGeometryContextImpl.cs" />
     <Compile Include="Media\GeometryImpl.cs" />

+ 14 - 9
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -27,7 +27,7 @@ namespace Avalonia
 
 namespace Avalonia.Direct2D1
 {
-    public class Direct2D1Platform : IPlatformRenderInterface, IRendererFactory
+    public class Direct2D1Platform : IPlatformRenderInterface
     {
         private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
 
@@ -76,7 +76,6 @@ namespace Avalonia.Direct2D1
         {
             AvaloniaLocator.CurrentMutable
                         .Bind<IPlatformRenderInterface>().ToConstant(s_instance)
-                        .Bind<IRendererFactory>().ToConstant(s_instance)
                         .BindToSelf(s_d2D1Factory)
                         .BindToSelf(s_dwfactory)
                         .BindToSelf(s_imagingFactory)
@@ -107,11 +106,6 @@ namespace Avalonia.Direct2D1
                 spans);
         }
 
-        public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
-        {
-            return new Renderer(root, renderLoop);
-        }
-
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
             var nativeWindow = surfaces?.OfType<IPlatformHandle>().FirstOrDefault();
@@ -124,9 +118,20 @@ namespace Avalonia.Direct2D1
             throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from any of provided surfaces");
         }
 
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
+            int width,
+            int height,
+            double dpiX,
+            double dpiY)
         {
-            return new RenderTargetBitmapImpl(s_imagingFactory, s_d2D1Device.Factory, width, height);
+            return new RenderTargetBitmapImpl(
+                s_imagingFactory,
+                s_d2D1Factory,
+                s_dwfactory,
+                width,
+                height,
+                dpiX,
+                dpiY);
         }
 
         public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)

+ 79 - 26
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -6,6 +6,8 @@ using System.Collections;
 using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.RenderHelpers;
+using Avalonia.Rendering;
 using SharpDX;
 using SharpDX.Direct2D1;
 using SharpDX.Mathematics.Interop;
@@ -18,30 +20,27 @@ namespace Avalonia.Direct2D1.Media
     /// </summary>
     public class DrawingContextImpl : IDrawingContextImpl, IDisposable
     {
-        /// <summary>
-        /// The Direct2D1 render target.
-        /// </summary>
+        private readonly IVisualBrushRenderer _visualBrushRenderer;
         private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
-
-        /// <summary>
-        /// The DirectWrite factory.
-        /// </summary>
+        private readonly SharpDX.DXGI.SwapChain1 _swapChain;
         private SharpDX.DirectWrite.Factory _directWriteFactory;
 
-        private SharpDX.DXGI.SwapChain1 _swapChain;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="DrawingContextImpl"/> class.
         /// </summary>
+        /// <param name="visualBrushRenderer">The visual brush renderer.</param>
         /// <param name="renderTarget">The render target to draw to.</param>
         /// <param name="directWriteFactory">The DirectWrite factory.</param>
         /// <param name="swapChain">An optional swap chain associated with this drawing context.</param>
         public DrawingContextImpl(
+            IVisualBrushRenderer visualBrushRenderer,
             SharpDX.Direct2D1.RenderTarget renderTarget,
             SharpDX.DirectWrite.Factory directWriteFactory,
             SharpDX.DXGI.SwapChain1 swapChain = null)
         {
+            _visualBrushRenderer = visualBrushRenderer;
             _renderTarget = renderTarget;
+            _swapChain = swapChain;
             _directWriteFactory = directWriteFactory;
             _swapChain = swapChain;
             _renderTarget.BeginDraw();
@@ -72,10 +71,10 @@ namespace Avalonia.Direct2D1.Media
             try
             {
                 _renderTarget.EndDraw();
-                
+
                 _swapChain?.Present(1, SharpDX.DXGI.PresentFlags.None);
             }
-            catch (SharpDXException ex) when((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET
+            catch (SharpDXException ex) when ((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET
             {
                 throw new RenderTargetCorruptedException(ex);
             }
@@ -90,14 +89,38 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="destRect">The rect in the output to draw to.</param>
         public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
         {
-            var impl = (BitmapImpl)source;
-            Bitmap d2d = impl.GetDirect2DBitmap(_renderTarget);
-            _renderTarget.DrawBitmap(
-                d2d,
-                destRect.ToSharpDX(),
-                (float)opacity,
-                BitmapInterpolationMode.Linear,
-                sourceRect.ToSharpDX());
+            using (var d2d = ((BitmapImpl)source).GetDirect2DBitmap(_renderTarget))
+            {
+                _renderTarget.DrawBitmap(
+                    d2d,
+                    destRect.ToSharpDX(),
+                    (float)opacity,
+                    BitmapInterpolationMode.Linear,
+                    sourceRect.ToSharpDX());
+            }
+        }
+
+        /// <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>
+        public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+        {
+            using (var d2dSource = ((BitmapImpl)source).GetDirect2DBitmap(_renderTarget))
+            using (var sourceBrush = new BitmapBrush(_renderTarget, d2dSource))
+            using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size))
+            using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_renderTarget.Factory, destRect.ToDirect2D()))
+            {
+                d2dOpacityMask.PlatformBrush.Transform = Matrix.CreateTranslation(opacityMaskRect.Position).ToDirect2D();
+
+                _renderTarget.FillGeometry(
+                    geometry,
+                    sourceBrush,
+                    d2dOpacityMask.PlatformBrush);
+            }
         }
 
         /// <summary>
@@ -283,7 +306,7 @@ namespace Avalonia.Direct2D1.Media
                 {
                     ContentBounds = PrimitiveExtensions.RectangleInfinite,
                     MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
-                    Opacity = (float) opacity,
+                    Opacity = (float)opacity,
                 };
 
                 var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget);
@@ -338,16 +361,46 @@ namespace Avalonia.Direct2D1.Media
             }
             else if (imageBrush != null)
             {
-                return new TileBrushImpl(imageBrush, _renderTarget, destinationSize);
+                return new ImageBrushImpl(
+                    imageBrush,
+                    _renderTarget,
+                    (BitmapImpl)imageBrush.Source.PlatformImpl,
+                    destinationSize);
             }
             else if (visualBrush != null)
             {
-                return new TileBrushImpl(visualBrush, _renderTarget, destinationSize);
-            }
-            else
-            {
-                return new SolidColorBrushImpl(null, _renderTarget);
+                if (_visualBrushRenderer != null)
+                {
+                    var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+
+                    if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
+                    {
+                        using (var intermediate = new BitmapRenderTarget(
+                            _renderTarget,
+                            CompatibleRenderTargetOptions.None,
+                            intermediateSize.ToSharpDX()))
+                        {
+                            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer))
+                            {
+                                intermediate.Clear(null);
+                                _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
+                            }
+
+                            return new ImageBrushImpl(
+                                visualBrush,
+                                _renderTarget,
+                                new D2DBitmapImpl(intermediate.Bitmap),
+                                destinationSize);
+                        }
+                    }
+                }
+                else
+                {
+                    throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+                }
             }
+
+            return new SolidColorBrushImpl(null, _renderTarget);
         }
 
         public void PushGeometryClip(IGeometryImpl clip)

+ 58 - 29
src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs → src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@@ -1,51 +1,48 @@
 // 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.RenderHelpers;
+using Avalonia.Rendering.Utilities;
 using SharpDX.Direct2D1;
 
 namespace Avalonia.Direct2D1.Media
 {
-    public sealed class TileBrushImpl : BrushImpl
+    public sealed class ImageBrushImpl : BrushImpl
     {
-        public TileBrushImpl(
+        public ImageBrushImpl(
             ITileBrush brush,
             SharpDX.Direct2D1.RenderTarget target,
+            BitmapImpl bitmap,
             Size targetSize)
         {
-            var helper = new TileBrushImplHelper(brush, targetSize);
-            if (!helper.IsValid)
-                return;
+            var calc = new TileBrushCalculator(brush, new Size(bitmap.PixelWidth, bitmap.PixelHeight), targetSize);
 
-            using (var intermediate = new BitmapRenderTarget(target, CompatibleRenderTargetOptions.None, helper.IntermediateSize.ToSharpDX()))
+            if (!calc.NeedsIntermediate)
             {
-                using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
-                {
-                    intermediate.Clear(null);
-                    helper.DrawIntermediate(ctx);
-                }
-
                 PlatformBrush = new BitmapBrush(
                     target,
-                    intermediate.Bitmap,
+                    bitmap.GetDirect2DBitmap(target),
                     GetBitmapBrushProperties(brush),
-                    GetBrushProperties(brush, helper.DestinationRect));
+                    GetBrushProperties(brush, calc.DestinationRect));
+            }
+            else
+            {
+                using (var intermediate = RenderIntermediate(target, bitmap, calc))
+                {
+                    PlatformBrush = new BitmapBrush(
+                        target,
+                        intermediate.Bitmap,
+                        GetBitmapBrushProperties(brush),
+                        GetBrushProperties(brush, calc.DestinationRect));
+                }
             }
         }
 
-        private static BrushProperties GetBrushProperties(ITileBrush brush, Rect destinationRect)
+        public override void Dispose()
         {
-            var tileTransform = 
-                brush.TileMode != TileMode.None ? 
-                Matrix.CreateTranslation(destinationRect.X, destinationRect.Y) :
-                Matrix.Identity;
-
-            return new BrushProperties
-            {
-                Opacity = (float)brush.Opacity,
-                Transform = tileTransform.ToDirect2D(),
-            };
+            ((BitmapBrush)PlatformBrush)?.Bitmap.Dispose();
+            base.Dispose();
         }
 
         private static BitmapBrushProperties GetBitmapBrushProperties(ITileBrush brush)
@@ -59,6 +56,20 @@ namespace Avalonia.Direct2D1.Media
             };
         }
 
+        private static BrushProperties GetBrushProperties(ITileBrush brush, Rect destinationRect)
+        {
+            var tileTransform =
+                brush.TileMode != TileMode.None ?
+                Matrix.CreateTranslation(destinationRect.X, destinationRect.Y) :
+                Matrix.Identity;
+
+            return new BrushProperties
+            {
+                Opacity = (float)brush.Opacity,
+                Transform = tileTransform.ToDirect2D(),
+            };
+        }
+
         private static ExtendMode GetExtendModeX(TileMode tileMode)
         {
             return (tileMode & TileMode.FlipX) != 0 ? ExtendMode.Mirror : ExtendMode.Wrap;
@@ -69,10 +80,28 @@ namespace Avalonia.Direct2D1.Media
             return (tileMode & TileMode.FlipY) != 0 ? ExtendMode.Mirror : ExtendMode.Wrap;
         }
 
-        public override void Dispose()
+        private BitmapRenderTarget RenderIntermediate(
+            SharpDX.Direct2D1.RenderTarget target,
+            BitmapImpl bitmap,
+            TileBrushCalculator calc)
         {
-            ((BitmapBrush)PlatformBrush)?.Bitmap.Dispose();
-            base.Dispose();
+            var result = new BitmapRenderTarget(
+                target,
+                CompatibleRenderTargetOptions.None,
+                calc.IntermediateSize.ToSharpDX());
+
+            using (var context = new RenderTarget(result).CreateDrawingContext(null))
+            {
+                var rect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
+
+                context.Clear(Colors.Transparent);
+                context.PushClip(calc.IntermediateClip);
+                context.Transform = calc.IntermediateTransform;
+                context.DrawImage(bitmap, 1, rect, rect);
+                context.PopClip();
+            }
+
+            return result;
         }
     }
 }

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

@@ -2,35 +2,41 @@
 // 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;
 using SharpDX.Direct2D1;
 using SharpDX.WIC;
+using DirectWriteFactory = SharpDX.DirectWrite.Factory;
 
 namespace Avalonia.Direct2D1.Media
 {
     public class RenderTargetBitmapImpl : WicBitmapImpl, IRenderTargetBitmapImpl
     {
+        private readonly DirectWriteFactory _dwriteFactory;
         private readonly WicRenderTarget _target;
 
         public RenderTargetBitmapImpl(
             ImagingFactory imagingFactory,
             Factory d2dFactory,
+            DirectWriteFactory dwriteFactory,
             int width,
-            int height)
+            int height,
+            double dpiX,
+            double dpiY)
             : base(imagingFactory, width, height)
         {
             var props = new RenderTargetProperties
             {
-                DpiX = 96,
-                DpiY = 96,
+                DpiX = (float)dpiX,
+                DpiY = (float)dpiY,
             };
 
             _target = new WicRenderTarget(
                 d2dFactory,
                 WicImpl,
                 props);
+
+            _dwriteFactory = dwriteFactory;
         }
 
         public override void Dispose()
@@ -39,7 +45,9 @@ namespace Avalonia.Direct2D1.Media
             base.Dispose();
         }
 
-        public Avalonia.Media.DrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext();
-        
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        {
+            return new DrawingContextImpl(visualBrushRenderer, _target, _dwriteFactory);
+        }
     }
 }

+ 3 - 8
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs

@@ -116,14 +116,9 @@ namespace Avalonia.Direct2D1.Media
         /// <returns>The Direct2D bitmap.</returns>
         public override SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget)
         {
-            if (_direct2D == null)
-            {
-                FormatConverter converter = new FormatConverter(_factory);
-                converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA);
-                _direct2D = SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
-            }
-
-            return _direct2D;
+            FormatConverter converter = new FormatConverter(_factory);
+            converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA);
+            return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
         }
 
         /// <summary>

+ 5 - 6
src/Windows/Avalonia.Direct2D1/RenderTarget.cs

@@ -2,11 +2,10 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Direct2D1.Media;
 using Avalonia.Platform;
-using Avalonia.Win32.Interop;
-using SharpDX;
+using Avalonia.Rendering;
 using SharpDX.Direct2D1;
-using DrawingContext = Avalonia.Media.DrawingContext;
 using DwFactory = SharpDX.DirectWrite.Factory;
 
 namespace Avalonia.Direct2D1
@@ -48,10 +47,10 @@ namespace Avalonia.Direct2D1
         /// <summary>
         /// Creates a drawing context for a rendering session.
         /// </summary>
-        /// <returns>An <see cref="Avalonia.Media.DrawingContext"/>.</returns>
-        public DrawingContext CreateDrawingContext()
+        /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
-            return new DrawingContext(new Media.DrawingContextImpl(_renderTarget, DirectWriteFactory));
+            return new DrawingContextImpl(visualBrushRenderer, _renderTarget, DirectWriteFactory);
         }
 
         public void Dispose()

+ 9 - 9
src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs

@@ -1,11 +1,5 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia.Media;
 using Avalonia.Platform;
-using Avalonia.Win32.Interop;
 using SharpDX;
 using SharpDX.Direct2D1;
 using SharpDX.DXGI;
@@ -14,6 +8,8 @@ using AlphaMode = SharpDX.Direct2D1.AlphaMode;
 using Device = SharpDX.Direct2D1.Device;
 using Factory = SharpDX.Direct2D1.Factory;
 using Factory2 = SharpDX.DXGI.Factory2;
+using Avalonia.Rendering;
+using Avalonia.Direct2D1.Media;
 
 namespace Avalonia.Direct2D1
 {
@@ -56,8 +52,8 @@ namespace Avalonia.Direct2D1
         /// <summary>
         /// Creates a drawing context for a rendering session.
         /// </summary>
-        /// <returns>An <see cref="Avalonia.Media.DrawingContext"/>.</returns>
-        public DrawingContext CreateDrawingContext()
+        /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
             var size = GetWindowSize();
             var dpi = GetWindowDpi();
@@ -69,7 +65,11 @@ namespace Avalonia.Direct2D1
                 CreateSwapChain();
             }
 
-            return new DrawingContext(new Media.DrawingContextImpl(_deviceContext, DirectWriteFactory, _swapChain));
+            return new DrawingContextImpl(
+                visualBrushRenderer,
+                _deviceContext,
+                DirectWriteFactory,
+                _swapChain);
         }
 
         public void Dispose()

+ 7 - 1
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -35,7 +35,7 @@ namespace Avalonia
 
 namespace Avalonia.Win32
 {
-    partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
+    partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IRendererFactory
     {
         private static readonly Win32Platform s_instance = new Win32Platform();
         private static uint _uiThread;
@@ -70,6 +70,7 @@ 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);
@@ -197,5 +198,10 @@ namespace Avalonia.Win32
         {
             return new PopupImpl();
         }
+
+        public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
+        {
+            return new ImmediateRenderer(root);
+        }
     }
 }

+ 0 - 509
tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs

@@ -1,509 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Avalonia.Controls;
-using Avalonia.Controls.Presenters;
-using Avalonia.Layout;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.UnitTests;
-using Moq;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Xunit;
-
-namespace Avalonia.Input.UnitTests
-{
-    public class InputElement_HitTesting
-    {
-        [Fact]
-        public void InputHitTest_Should_Find_Control_At_Point()
-        {
-            using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                var container = new Decorator
-                {
-                    Width = 200,
-                    Height = 200,
-                    Child = new Border
-                    {
-                        Width = 100,
-                        Height = 100,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                        VerticalAlignment = VerticalAlignment.Center
-                    }
-                };
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(100, 100));
-
-                Assert.Equal(container.Child, result);
-            }
-        }
-
-        [Fact]
-        public void InputHitTest_Should_Not_Find_Control_Outside_Point()
-        {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                var container = new Decorator
-                {
-                    Width = 200,
-                    Height = 200,
-                    Child = new Border
-                    {
-                        Width = 100,
-                        Height = 100,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                        VerticalAlignment = VerticalAlignment.Center
-                    }
-                };
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(10, 10));
-
-                Assert.Equal(container, result);
-            }
-        }
-
-        [Fact]
-        public void InputHitTest_Should_Find_Top_Control_At_Point()
-        {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                var container = new Panel
-                {
-                    Width = 200,
-                    Height = 200,
-                    Children = new Controls.Controls
-                {
-                    new Border
-                    {
-                        Width = 100,
-                        Height = 100,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                        VerticalAlignment = VerticalAlignment.Center
-                    },
-                    new Border
-                    {
-                        Width = 50,
-                        Height = 50,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                        VerticalAlignment = VerticalAlignment.Center
-                    }
-                }
-                };
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(100, 100));
-
-                Assert.Equal(container.Children[1], result);
-            }
-        }
-
-        [Fact]
-        public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder()
-        {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                var container = new Panel
-                {
-                    Width = 200,
-                    Height = 200,
-                    Children = new Controls.Controls
-                {
-                    new Border
-                    {
-                        Width = 100,
-                        Height = 100,
-                        ZIndex = 1,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                        VerticalAlignment = VerticalAlignment.Center
-                    },
-                    new Border
-                    {
-                        Width = 50,
-                        Height = 50,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                        VerticalAlignment = VerticalAlignment.Center
-                    }
-                }
-                };
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(100, 100));
-
-                Assert.Equal(container.Children[0], result);
-            }
-        }
-
-        [Fact]
-        public void InputHitTest_Should_Find_Control_Translated_Outside_Parent_Bounds()
-        {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                Border target;
-                var container = new Panel
-                {
-                    Width = 200,
-                    Height = 200,
-                    ClipToBounds = false,
-                    Children = new Controls.Controls
-                    {
-                        new Border
-                        {
-                            Width = 100,
-                            Height = 100,
-                            ZIndex = 1,
-                            HorizontalAlignment = HorizontalAlignment.Left,
-                            VerticalAlignment = VerticalAlignment.Top,
-                            Child = target = new Border
-                            {
-                                Width = 50,
-                                Height = 50,
-                                HorizontalAlignment = HorizontalAlignment.Left,
-                                VerticalAlignment = VerticalAlignment.Top,
-                                RenderTransform = new TranslateTransform(110, 110),
-                            }
-                        },
-                    }
-                };
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(120, 120));
-
-                Assert.Equal(target, result);
-            }
-        }
-
-        [Fact]
-        public void InputHitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
-        {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                Border target;
-
-                var container = new Panel
-                {
-                    Width = 100,
-                    Height = 200,
-                    Children = new Controls.Controls
-                    {
-                        new Panel()
-                        {
-                            Width = 100,
-                            Height = 100,
-                            Margin = new Thickness(0, 100, 0, 0),
-                            ClipToBounds = true,
-                            Children = new Controls.Controls
-                            {
-                                (target = new Border()
-                                {
-                                    Width = 100,
-                                    Height = 100,
-                                    Margin = new Thickness(0, -100, 0, 0)
-                                })
-                            }
-                        }
-                    }
-                };
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(50, 50));
-
-                Assert.NotEqual(target, result);
-                Assert.Equal(container, result);
-            }
-        }
-
-        [Fact]
-        public void InputHitTest_Should_Not_Find_Control_Outside_Scroll_ViewPort()
-        {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
-            {
-                Border target;
-                Border item1;
-                Border item2;
-                ScrollContentPresenter scroll;
-
-                var container = new Panel
-                {
-                    Width = 100,
-                    Height = 200,
-                    Children = new Controls.Controls
-                    {
-                        (target = new Border()
-                        {
-                            Width = 100,
-                            Height = 100
-                        }),
-                        new Border()
-                        {
-                            Width = 100,
-                            Height = 100,
-                            Margin = new Thickness(0, 100, 0, 0),
-                            Child = scroll = new ScrollContentPresenter()
-                            {
-                                Content = new StackPanel()
-                                {
-                                    Children = new Controls.Controls
-                                    {
-                                        (item1 = new Border()
-                                        {
-                                            Width = 100,
-                                            Height = 100,
-                                        }),
-                                        (item2 = new Border()
-                                        {
-                                            Width = 100,
-                                            Height = 100,
-                                        }),
-                                    }
-                                }
-                            }
-                        }
-                    }
-                };
-
-                scroll.UpdateChild();
-
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
-                var result = container.InputHitTest(new Point(50, 150));
-
-                Assert.Equal(item1, result);
-
-                result = container.InputHitTest(new Point(50, 50));
-
-                Assert.Equal(target, result);
-
-                scroll.Offset = new Vector(0, 100);
-
-                //we don't have setup LayoutManager so we will make it manually
-                scroll.Parent.InvalidateArrange();
-                container.InvalidateArrange();
-
-                container.Arrange(new Rect(container.DesiredSize));
-                context.Render(container);
-
-                result = container.InputHitTest(new Point(50, 150));
-
-                Assert.Equal(item2, result);
-
-                result = container.InputHitTest(new Point(50, 50));
-
-                Assert.NotEqual(item1, result);
-                Assert.Equal(target, result);
-            }
-        }
-
-        class MockRenderInterface : IPlatformRenderInterface
-        {
-            public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IStreamGeometryImpl CreateStreamGeometry()
-            {
-                return new MockStreamGeometry();
-            }
-
-            public IBitmapImpl LoadBitmap(Stream stream)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IBitmapImpl LoadBitmap(string fileName)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt)
-            {
-                throw new NotImplementedException();
-            }
-
-            class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl
-            {
-                private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
-                public Rect Bounds
-                {
-                    get
-                    {
-                        throw new NotImplementedException();
-                    }
-                }
-
-                public Matrix Transform
-                {
-                    get
-                    {
-                        throw new NotImplementedException();
-                    }
-
-                    set
-                    {
-                        throw new NotImplementedException();
-                    }
-                }
-
-                public IStreamGeometryImpl Clone()
-                {
-                    return this;
-                }
-
-                public bool FillContains(Point point)
-                {
-                    return _impl.FillContains(point);
-                }
-
-                public Rect GetRenderBounds(double strokeThickness)
-                {
-                    throw new NotImplementedException();
-                }
-
-                public IGeometryImpl Intersect(IGeometryImpl geometry)
-                {
-                    throw new NotImplementedException();
-                }
-
-                public IStreamGeometryContextImpl Open()
-                {
-                    return _impl;
-                }
-
-                public bool StrokeContains(Pen pen, Point point)
-                {
-                    throw new NotImplementedException();
-                }
-
-                public IGeometryImpl WithTransform(Matrix transform)
-                {
-                    throw new NotImplementedException();
-                }
-
-                class MockStreamGeometryContext : IStreamGeometryContextImpl
-                {
-                    private List<Point> points = new List<Point>();
-                    public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
-                    {
-                        throw new NotImplementedException();
-                    }
-
-                    public void BeginFigure(Point startPoint, bool isFilled)
-                    {
-                        points.Add(startPoint);
-                    }
-
-                    public void CubicBezierTo(Point point1, Point point2, Point point3)
-                    {
-                        throw new NotImplementedException();
-                    }
-
-                    public void Dispose()
-                    {
-                    }
-
-                    public void EndFigure(bool isClosed)
-                    {
-                    }
-
-                    public void LineTo(Point point)
-                    {
-                        points.Add(point);
-                    }
-
-                    public void QuadraticBezierTo(Point control, Point endPoint)
-                    {
-                        throw new NotImplementedException();
-                    }
-
-                    public void SetFillRule(FillRule fillRule)
-                    {
-                    }
-
-                    public bool FillContains(Point point)
-                    {
-                        // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html
-                        // to determine if the point is in the geometry (since it will always be convex in this situation)
-                        for (int i = 0; i < points.Count; i++)
-                        {
-                            var a = points[i];
-                            var b = points[(i + 1) % points.Count];
-                            var c = points[(i + 2) % points.Count];
-
-                            Vector v0 = c - a;
-                            Vector v1 = b - a;
-                            Vector v2 = point - a;
-
-                            var dot00 = v0 * v0;
-                            var dot01 = v0 * v1;
-                            var dot02 = v0 * v2;
-                            var dot11 = v1 * v1;
-                            var dot12 = v1 * v2;
-
-
-                            var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
-                            var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
-                            var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
-                            if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
-                        }
-                        return false;
-                    }
-                }
-            }
-        }
-    }
-}

+ 3 - 3
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@@ -79,7 +79,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             var fb = new Framebuffer(fmt, 80, 80);
             var r = Avalonia.AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
             using (var target = r.CreateRenderTarget(new object[] { fb }))
-            using (var ctx = target.CreateDrawingContext())
+            using (var ctx = target.CreateDrawingContext(null))
             {
                 ctx.PushOpacity(0.8);
                 ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100));
@@ -91,13 +91,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             fb.Deallocate();
             using (var rtb = new RenderTargetBitmap(100, 100))
             {
-                using (var ctx = rtb.CreateDrawingContext())
+                using (var ctx = rtb.CreateDrawingContext(null))
                 {
                     ctx.FillRectangle(Brushes.Blue, new Rect(0, 0, 100, 100));
                     ctx.FillRectangle(Brushes.Pink, new Rect(0, 20, 100, 10));
 
                     var rc = new Rect(0, 0, 60, 60);
-                    ctx.DrawImage(bmp, 1, rc, rc);
+                    ctx.DrawImage(bmp.PlatformImpl, 1, rc, rc);
                 }
                 rtb.Save(System.IO.Path.Combine(OutputPath, testName + ".out.png"));
             }

+ 0 - 13
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@@ -1,7 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
-    <EnableDefaultCompileItems>False</EnableDefaultCompileItems>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -31,18 +30,6 @@
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Xml" />
   </ItemGroup>
-  <ItemGroup>
-    <Compile Include="InvariantCultureFixture.cs" />
-    <Compile Include="MockRendererFactory.cs" />
-    <Compile Include="NotifyingBase.cs" />
-    <Compile Include="TestLogSink.cs" />
-    <Compile Include="TestTemplatedRoot.cs" />
-    <Compile Include="TestRoot.cs" />
-    <Compile Include="TestServices.cs" />
-    <Compile Include="UnitTestApplication.cs" />
-    <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="MockWindowingPlatform.cs" />
-  </ItemGroup>
   <ItemGroup>
     <ProjectReference Condition="'$(TargetFramework)' == 'net461'" Include="..\..\src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj" />
 	<ProjectReference Condition="'$(TargetFramework)' == 'netcoreapp1.1'" Include="..\..\src\Avalonia.DotNetCoreRuntime\Avalonia.DotNetCoreRuntime.csproj" />

+ 62 - 0
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Moq;
+
+namespace Avalonia.UnitTests
+{
+    public class MockPlatformRenderInterface : IPlatformRenderInterface
+    {
+        public IFormattedTextImpl CreateFormattedText(
+            string text,
+            Typeface typeface,
+            TextAlignment textAlignment,
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
+        {
+            return Mock.Of<IFormattedTextImpl>();
+        }
+
+        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
+        {
+            return Mock.Of<IRenderTarget>();
+        }
+
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
+            int width,
+            int height,
+            double dpiX,
+            double dpiY)
+        {
+            return Mock.Of<IRenderTargetBitmapImpl>();
+        }
+
+        public IStreamGeometryImpl CreateStreamGeometry()
+        {
+            return new MockStreamGeometryImpl();
+        }
+
+        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?))
+        {
+            throw new NotImplementedException();
+        }
+
+        public IBitmapImpl LoadBitmap(Stream stream)
+        {
+            return Mock.Of<IBitmapImpl>();
+        }
+
+        public IBitmapImpl LoadBitmap(string fileName)
+        {
+            return Mock.Of<IBitmapImpl>();
+        }
+
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 152 - 0
tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs

@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+    public class MockStreamGeometryImpl : IStreamGeometryImpl
+    {
+        private MockStreamGeometryContext _context;
+
+        public MockStreamGeometryImpl()
+        {
+            Transform = Matrix.Identity;
+            _context = new MockStreamGeometryContext();
+        }
+
+        public MockStreamGeometryImpl(Matrix transform)
+        {
+            Transform = transform;
+            _context = new MockStreamGeometryContext();
+        }
+
+        private MockStreamGeometryImpl(Matrix transform, MockStreamGeometryContext context)
+        {
+            Transform = transform;
+            _context = context;
+        }
+
+        public Rect Bounds => _context.CalculateBounds();
+
+        public Matrix Transform { get; }
+
+        public IStreamGeometryImpl Clone()
+        {
+            return this;
+        }
+
+        public bool FillContains(Point point)
+        {
+            return _context.FillContains(point);
+        }
+
+        public bool StrokeContains(Pen pen, Point point)
+        {
+            return false;
+        }
+
+        public Rect GetRenderBounds(double strokeThickness) => Bounds;
+
+        public IGeometryImpl Intersect(IGeometryImpl geometry)
+        {
+            return new MockStreamGeometryImpl(Transform);
+        }
+
+        public IStreamGeometryContextImpl Open()
+        {
+            return _context;
+        }
+
+        public IGeometryImpl WithTransform(Matrix transform)
+        {
+            return new MockStreamGeometryImpl(transform, _context);
+        }
+
+        class MockStreamGeometryContext : IStreamGeometryContextImpl
+        {
+            private List<Point> points = new List<Point>();
+            public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+            {
+            }
+
+            public void BeginFigure(Point startPoint, bool isFilled)
+            {
+                points.Add(startPoint);
+            }
+
+            public Rect CalculateBounds()
+            {
+                var left = double.MaxValue;
+                var right = double.MinValue;
+                var top = double.MaxValue;
+                var bottom = double.MinValue;
+
+                foreach (var p in points)
+                {
+                    left = Math.Min(p.X, left);
+                    right = Math.Max(p.X, right);
+                    top = Math.Min(p.Y, top);
+                    bottom = Math.Max(p.Y, bottom);
+                }
+
+                return new Rect(new Point(left, top), new Point(right, bottom));
+            }
+
+            public void CubicBezierTo(Point point1, Point point2, Point point3)
+            {
+            }
+
+            public void Dispose()
+            {
+            }
+
+            public void EndFigure(bool isClosed)
+            {
+            }
+
+            public void LineTo(Point point)
+            {
+                points.Add(point);
+            }
+
+            public void QuadraticBezierTo(Point control, Point endPoint)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void SetFillRule(FillRule fillRule)
+            {
+            }
+
+            public bool FillContains(Point point)
+            {
+                // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html
+                // to determine if the point is in the geometry (since it will always be convex in this situation)
+                for (int i = 0; i < points.Count; i++)
+                {
+                    var a = points[i];
+                    var b = points[(i + 1) % points.Count];
+                    var c = points[(i + 2) % points.Count];
+
+                    Vector v0 = c - a;
+                    Vector v1 = b - a;
+                    Vector v2 = point - a;
+
+                    var dot00 = v0 * v0;
+                    var dot01 = v0 * v1;
+                    var dot02 = v0 * v2;
+                    var dot11 = v1 * v1;
+                    var dot12 = v1 * v2;
+
+
+                    var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
+                    var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
+                    var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
+                    if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
+                }
+                return false;
+            }
+        }
+    }
+}

+ 4 - 4
tests/Avalonia.UnitTests/TestServices.cs

@@ -23,7 +23,7 @@ namespace Avalonia.UnitTests
             assetLoader: new AssetLoader(),
             layoutManager: new LayoutManager(),
             platform: new AppBuilder().RuntimePlatform,
-            renderer: Mock.Of<IRenderer>(),
+            renderer: (_, __) => Mock.Of<IRenderer>(),
             renderInterface: CreateRenderInterfaceMock(),
             renderLoop: Mock.Of<IRenderLoop>(),
             standardCursorFactory: Mock.Of<IStandardCursorFactory>(),
@@ -65,7 +65,7 @@ namespace Avalonia.UnitTests
             Func<IKeyboardDevice> keyboardDevice = null,
             ILayoutManager layoutManager = null,
             IRuntimePlatform platform = null,
-            IRenderer renderer = null,
+            Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
             IPlatformRenderInterface renderInterface = null,
             IRenderLoop renderLoop = null,
             IScheduler scheduler = null,
@@ -98,7 +98,7 @@ namespace Avalonia.UnitTests
         public Func<IKeyboardDevice> KeyboardDevice { get; }
         public ILayoutManager LayoutManager { get; }
         public IRuntimePlatform Platform { get; }
-        public IRenderer Renderer { get; }
+        public Func<IRenderRoot, IRenderLoop, IRenderer> Renderer { get; }
         public IPlatformRenderInterface RenderInterface { get; }
         public IRenderLoop RenderLoop { get; }
         public IScheduler Scheduler { get; }
@@ -115,7 +115,7 @@ namespace Avalonia.UnitTests
             Func<IKeyboardDevice> keyboardDevice = null,
             ILayoutManager layoutManager = null,
             IRuntimePlatform platform = null,
-            IRenderer renderer = null,
+            Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
             IPlatformRenderInterface renderInterface = null,
             IRenderLoop renderLoop = null,
             IScheduler scheduler = null,

+ 16 - 1
tests/Avalonia.UnitTests/UnitTestApplication.cs

@@ -51,7 +51,7 @@ namespace Avalonia.UnitTests
                 .Bind<IKeyboardDevice>().ToConstant(Services.KeyboardDevice?.Invoke())
                 .Bind<ILayoutManager>().ToConstant(Services.LayoutManager)
                 .Bind<IRuntimePlatform>().ToConstant(Services.Platform)
-                .Bind<IRenderer>().ToConstant(Services.Renderer)
+                .Bind<IRendererFactory>().ToConstant(new RendererFactory(Services.Renderer))
                 .Bind<IPlatformRenderInterface>().ToConstant(Services.RenderInterface)
                 .Bind<IRenderLoop>().ToConstant(Services.RenderLoop)
                 .Bind<IPlatformThreadingInterface>().ToConstant(Services.ThreadingInterface)
@@ -67,5 +67,20 @@ namespace Avalonia.UnitTests
                 Styles.AddRange(styles);
             }
         }
+
+        private class RendererFactory : IRendererFactory
+        {
+            Func<IRenderRoot, IRenderLoop, IRenderer> _func;
+
+            public RendererFactory(Func<IRenderRoot, IRenderLoop, IRenderer> func)
+            {
+                _func = func;
+            }
+
+            public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
+            {
+                return _func?.Invoke(root, renderLoop);
+            }
+        }
     }
 }

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

@@ -163,7 +163,7 @@ namespace Avalonia.Visuals.UnitTests
             var ctx = CreateDrawingContext();
             control.Measure(Size.Infinity);
             control.Arrange(new Rect(control.DesiredSize));
-            ctx.Render(control);
+            ImmediateRenderer.Render(control, ctx);
         }
 
         private DrawingContext CreateDrawingContext()

+ 0 - 39
tests/Avalonia.Visuals.UnitTests/TestRoot.cs

@@ -1,39 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-
-namespace Avalonia.Visuals.UnitTests
-{
-    public class TestRoot : TestVisual, IRenderRoot
-    {
-        public Size ClientSize { get; }
-        
-        public IRenderTarget CreateRenderTarget()
-        {
-            throw new NotImplementedException();
-        }
-
-        public void Invalidate(Rect rect)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IRenderer Renderer
-        {
-            get { throw new NotImplementedException(); }
-        }
-
-        public Point PointToClient(Point p)
-        {
-            throw new NotImplementedException();
-        }
-
-        public Point PointToScreen(Point p)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

+ 94 - 11
tests/Avalonia.Visuals.UnitTests/VisualTests.cs

@@ -5,7 +5,10 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
 using Avalonia.VisualTree;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Visuals.UnitTests
@@ -66,14 +69,25 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         public void Adding_Children_Should_Fire_OnAttachedToVisualTree()
         {
-            var child2 = new TestVisual();
-            var child1 = new TestVisual { Child = child2 };
+            var child2 = new Decorator();
+            var child1 = new Decorator { Child = child2 };
             var root = new TestRoot();
             var called1 = false;
             var called2 = false;
 
-            child1.AttachedToVisualTree += (s, e) => called1 = true;
-            child2.AttachedToVisualTree += (s, e) => called2 = true;
+            child1.AttachedToVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called1 = true;
+            };
+
+            child2.AttachedToVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called2 = true;
+            };
 
             root.Child = child1;
 
@@ -84,31 +98,100 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         public void Removing_Children_Should_Fire_OnDetachedFromVisualTree()
         {
-            var child2 = new TestVisual();
-            var child1 = new TestVisual { Child = child2 };
+            var child2 = new Decorator();
+            var child1 = new Decorator { Child = child2 };
             var root = new TestRoot();
             var called1 = false;
             var called2 = false;
 
             root.Child = child1;
-            child1.DetachedFromVisualTree += (s, e) => called1 = true;
-            child2.DetachedFromVisualTree += (s, e) => called2 = true;
+
+            child1.DetachedFromVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called1 = true;
+            };
+
+            child2.DetachedFromVisualTree += (s, e) =>
+            {
+                Assert.Equal(e.Parent, root);
+                Assert.Equal(e.Root, root);
+                called2 = true;
+            };
+
             root.Child = null;
 
             Assert.True(called1);
             Assert.True(called2);
         }
 
+        [Fact]
+        public void Root_Should_Retun_Self_As_VisualRoot()
+        {
+            var root = new TestRoot();
+
+            Assert.Same(root, ((IVisual)root).VisualRoot);
+        }
+
+        [Fact]
+        public void Descendents_Should_RetunVisualRoot()
+        {
+            var root = new TestRoot();
+            var child1 = new Decorator();
+            var child2 = new Decorator();
+
+            root.Child = child1;
+            child1.Child = child2;
+
+            Assert.Same(root, ((IVisual)child1).VisualRoot);
+            Assert.Same(root, ((IVisual)child2).VisualRoot);
+        }
+
+        [Fact]
+        public void Attaching_To_Visual_Tree_Should_Invalidate_Visual()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (UnitTestApplication.Start(new TestServices(renderer: (root, loop) => renderer.Object)))
+            {
+                var child = new Decorator();
+                var root = new TestRoot();
+
+                root.Child = child;
+
+                renderer.Verify(x => x.AddDirty(child));
+            }
+        }
+
+        [Fact]
+        public void Detaching_From_Visual_Tree_Should_Invalidate_Visual()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (UnitTestApplication.Start(new TestServices(renderer: (root, loop) => renderer.Object)))
+            {
+                var child = new Decorator();
+                var root = new TestRoot();
+
+                root.Child = child;
+                renderer.ResetCalls();
+                root.Child = null;
+
+                renderer.Verify(x => x.AddDirty(child));
+            }
+        }
+
         [Fact]
         public void Adding_Already_Parented_Control_Should_Throw()
         {
             var root1 = new TestRoot();
             var root2 = new TestRoot();
-            var child = new TestVisual();
+            var child = new Canvas();
 
-            root1.AddChild(child);
+            root1.Child = child;
 
-            Assert.Throws<InvalidOperationException>(() => root2.AddChild(child));
+            Assert.Throws<InvalidOperationException>(() => root2.Child = child);
             Assert.Equal(0, root2.GetVisualChildren().Count());
         }
     }

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/VisualTree/BoundsTrackerTests.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
 
                 tree.Measure(Size.Infinity);
                 tree.Arrange(new Rect(0, 0, 100, 100));
-                context.Render(tree);
+                ImmediateRenderer.Render(tree, context);
 
                 var track = target.Track(control);
                 var results = new List<TransformedBounds?>();

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height, double dpiX, double dpiY)
         {
             throw new NotImplementedException();
         }

+ 276 - 154
tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

@@ -12,6 +12,8 @@ using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Moq;
 using Xunit;
+using System;
+using Avalonia.Controls.Shapes;
 
 namespace Avalonia.Visuals.UnitTests.VisualTree
 {
@@ -20,9 +22,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
         [Fact]
         public void GetVisualsAt_Should_Find_Controls_At_Point()
         {
-            using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
-                var container = new Decorator
+                var container = new TestRoot
                 {
                     Width = 200,
                     Height = 200,
@@ -30,6 +32,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                     {
                         Width = 100,
                         Height = 100,
+                        Background = Brushes.Red,
                         HorizontalAlignment = HorizontalAlignment.Center,
                         VerticalAlignment = VerticalAlignment.Center
                     }
@@ -38,21 +41,46 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                 container.Measure(Size.Infinity);
                 container.Arrange(new Rect(container.DesiredSize));
 
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
+                var result = container.GetVisualsAt(new Point(100, 100));
+
+                Assert.Equal(new[] { container.Child }, result);
+            }
+        }
+
+        [Fact]
+        public void GetVisualsAt_Should_Not_Find_Empty_Controls_At_Point()
+        {
+            using (TestApplication())
+            {
+                var container = new TestRoot
+                {
+                    Width = 200,
+                    Height = 200,
+                    Child = new Border
+                    {
+                        Width = 100,
+                        Height = 100,
+                        HorizontalAlignment = HorizontalAlignment.Center,
+                        VerticalAlignment = VerticalAlignment.Center
+                    }
+                };
+
+                container.Measure(Size.Infinity);
+                container.Arrange(new Rect(container.DesiredSize));
 
                 var result = container.GetVisualsAt(new Point(100, 100));
 
-                Assert.Equal(new[] { container.Child, container }, result);
+                Assert.Empty(result);
             }
         }
 
         [Fact]
         public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point()
         {
-            using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
-                var container = new Decorator
+                Border visible;
+                var container = new TestRoot
                 {
                     Width = 200,
                     Height = 200,
@@ -60,11 +88,13 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                     {
                         Width = 100,
                         Height = 100,
+                        Background = Brushes.Red,
                         HorizontalAlignment = HorizontalAlignment.Center,
                         VerticalAlignment = VerticalAlignment.Center,
                         IsVisible = false,
-                        Child = new Border
+                        Child = visible = new Border
                         {
+                            Background = Brushes.Red,
                             HorizontalAlignment = HorizontalAlignment.Stretch,
                             VerticalAlignment = VerticalAlignment.Stretch,
                         }
@@ -74,21 +104,18 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                 container.Measure(Size.Infinity);
                 container.Arrange(new Rect(container.DesiredSize));
 
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
                 var result = container.GetVisualsAt(new Point(100, 100));
 
-                Assert.Equal(new[] { container }, result);
+                Assert.Empty(result);
             }
         }
 
         [Fact]
         public void GetVisualsAt_Should_Not_Find_Control_Outside_Point()
         {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
-                var container = new Decorator
+                var container = new TestRoot
                 {
                     Width = 200,
                     Height = 200,
@@ -96,6 +123,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                     {
                         Width = 100,
                         Height = 100,
+                        Background = Brushes.Red,
                         HorizontalAlignment = HorizontalAlignment.Center,
                         VerticalAlignment = VerticalAlignment.Center
                     }
@@ -104,142 +132,150 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                 container.Measure(Size.Infinity);
                 container.Arrange(new Rect(container.DesiredSize));
 
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
                 var result = container.GetVisualsAt(new Point(10, 10));
 
-                Assert.Equal(new[] { container }, result);
+                Assert.Empty(result);
             }
         }
 
         [Fact]
         public void GetVisualsAt_Should_Return_Top_Controls_First()
         {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
-                var container = new Panel
+                Panel container;
+                var root = new TestRoot
                 {
-                    Width = 200,
-                    Height = 200,
-                    Children = new Controls.Controls
+                    Child = container = new Panel
                     {
-                        new Border
+                        Width = 200,
+                        Height = 200,
+                        Children = new Controls.Controls
                         {
-                            Width = 100,
-                            Height = 100,
-                            HorizontalAlignment = HorizontalAlignment.Center,
-                            VerticalAlignment = VerticalAlignment.Center
-                        },
-                        new Border
-                        {
-                            Width = 50,
-                            Height = 50,
-                            HorizontalAlignment = HorizontalAlignment.Center,
-                            VerticalAlignment = VerticalAlignment.Center
+                            new Border
+                            {
+                                Width = 100,
+                                Height = 100,
+                                Background = Brushes.Red,
+                                HorizontalAlignment = HorizontalAlignment.Center,
+                                VerticalAlignment = VerticalAlignment.Center
+                            },
+                            new Border
+                            {
+                                Width = 50,
+                                Height = 50,
+                                Background = Brushes.Red,
+                                HorizontalAlignment = HorizontalAlignment.Center,
+                                VerticalAlignment = VerticalAlignment.Center
+                            }
                         }
                     }
                 };
 
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(container.DesiredSize));
 
                 var result = container.GetVisualsAt(new Point(100, 100));
 
-                Assert.Equal(new[] { container.Children[1], container.Children[0], container }, result);
+                Assert.Equal(new[] { container.Children[1], container.Children[0] }, result);
             }
         }
 
         [Fact]
         public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex()
         {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
-                var container = new Panel
+                Panel container;
+                var root = new TestRoot
                 {
-                    Width = 200,
-                    Height = 200,
-                    Children = new Controls.Controls
+                    Child = container = new Panel
                     {
-                        new Border
-                        {
-                            Width = 100,
-                            Height = 100,
-                            ZIndex = 1,
-                            HorizontalAlignment = HorizontalAlignment.Center,
-                            VerticalAlignment = VerticalAlignment.Center
-                        },
-                        new Border
-                        {
-                            Width = 50,
-                            Height = 50,
-                            HorizontalAlignment = HorizontalAlignment.Center,
-                            VerticalAlignment = VerticalAlignment.Center
-                        },
-                        new Border
+                        Width = 200,
+                        Height = 200,
+                        Children = new Controls.Controls
                         {
-                            Width = 75,
-                            Height = 75,
-                            ZIndex = 2,
-                            HorizontalAlignment = HorizontalAlignment.Center,
-                            VerticalAlignment = VerticalAlignment.Center
+                            new Border
+                            {
+                                Width = 100,
+                                Height = 100,
+                                ZIndex = 1,
+                                Background = Brushes.Red,
+                                HorizontalAlignment = HorizontalAlignment.Center,
+                                VerticalAlignment = VerticalAlignment.Center
+                            },
+                            new Border
+                            {
+                                Width = 50,
+                                Height = 50,
+                                Background = Brushes.Red,
+                                HorizontalAlignment = HorizontalAlignment.Center,
+                                VerticalAlignment = VerticalAlignment.Center
+                            },
+                            new Border
+                            {
+                                Width = 75,
+                                Height = 75,
+                                ZIndex = 2,
+                                Background = Brushes.Red,
+                                HorizontalAlignment = HorizontalAlignment.Center,
+                                VerticalAlignment = VerticalAlignment.Center
+                            }
                         }
                     }
                 };
 
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(container.DesiredSize));
 
                 var result = container.GetVisualsAt(new Point(100, 100));
 
-                Assert.Equal(new[] { container.Children[2], container.Children[0], container.Children[1], container }, result);
+                Assert.Equal(new[] { container.Children[2], container.Children[0], container.Children[1] }, result);
             }
         }
 
         [Fact]
         public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds()
         {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
                 Border target;
-                var container = new Panel
+                Panel container;
+                var root = new TestRoot
                 {
-                    Width = 200,
-                    Height = 200,
-                    ClipToBounds = false,
-                    Children = new Controls.Controls
+                    Child = container = new Panel
                     {
-                        new Border
+                        Width = 200,
+                        Height = 200,
+                        Background = Brushes.Red,
+                        ClipToBounds = false,
+                        Children = new Controls.Controls
                         {
-                            Width = 100,
-                            Height = 100,
-                            ZIndex = 1,
-                            HorizontalAlignment = HorizontalAlignment.Left,
-                            VerticalAlignment = VerticalAlignment.Top,
-                            Child = target = new Border
+                            new Border
                             {
-                                Width = 50,
-                                Height = 50,
+                                Width = 100,
+                                Height = 100,
+                                ZIndex = 1,
+                                Background = Brushes.Red,
                                 HorizontalAlignment = HorizontalAlignment.Left,
                                 VerticalAlignment = VerticalAlignment.Top,
-                                RenderTransform = new TranslateTransform(110, 110),
-                            }
-                        },
+                                Child = target = new Border
+                                {
+                                    Width = 50,
+                                    Height = 50,
+                                    Background = Brushes.Red,
+                                    HorizontalAlignment = HorizontalAlignment.Left,
+                                    VerticalAlignment = VerticalAlignment.Top,
+                                    RenderTransform = new TranslateTransform(110, 110),
+                                }
+                            },
+                        }
                     }
                 };
 
                 container.Measure(Size.Infinity);
                 container.Arrange(new Rect(container.DesiredSize));
 
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
-
                 var result = container.GetVisualsAt(new Point(120, 120));
 
                 Assert.Equal(new IVisual[] { target, container }, result);
@@ -249,40 +285,43 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
         [Fact]
         public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
         {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
                 Border target;
-
-                var container = new Panel
+                Panel container;
+                var root = new TestRoot
                 {
-                    Width = 100,
-                    Height = 200,
-                    Children = new Controls.Controls
+                    Child = container = new Panel
                     {
-                        new Panel()
+                        Width = 100,
+                        Height = 200,
+                        Background = Brushes.Red,
+                        Children = new Controls.Controls
                         {
-                            Width = 100,
-                            Height = 100,
-                            Margin = new Thickness(0, 100, 0, 0),
-                            ClipToBounds = true,
-                            Children = new Controls.Controls
+                            new Panel()
                             {
-                                (target = new Border()
+                                Width = 100,
+                                Height = 100,
+                                Background = Brushes.Red,
+                                Margin = new Thickness(0, 100, 0, 0),
+                                ClipToBounds = true,
+                                Children = new Controls.Controls
                                 {
-                                    Width = 100,
-                                    Height = 100,
-                                    Margin = new Thickness(0, -100, 0, 0)
-                                })
+                                    (target = new Border()
+                                    {
+                                        Width = 100,
+                                        Height = 100,
+                                        Background = Brushes.Red,
+                                        Margin = new Thickness(0, -100, 0, 0)
+                                    })
+                                }
                             }
                         }
                     }
                 };
 
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(container.DesiredSize));
 
                 var result = container.GetVisualsAt(new Point(50, 50));
 
@@ -293,45 +332,57 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
         [Fact]
         public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport()
         {
-            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
+            using (TestApplication())
             {
                 Border target;
                 Border item1;
                 Border item2;
                 ScrollContentPresenter scroll;
-
-                var container = new Panel
+                Panel container;
+                var root = new TestRoot
                 {
-                    Width = 100,
-                    Height = 200,
-                    Children = new Controls.Controls
+                    Child = container = new Panel
                     {
-                        (target = new Border()
-                        {
-                            Width = 100,
-                            Height = 100
-                        }),
-                        new Border()
+                        Width = 100,
+                        Height = 200,
+                        Background = Brushes.Red,
+                        Children = new Controls.Controls
                         {
-                            Width = 100,
-                            Height = 100,
-                            Margin = new Thickness(0, 100, 0, 0),
-                            Child = scroll = new ScrollContentPresenter()
+                            (target = new Border()
+                            {
+                                Name = "b1",
+                                Width = 100,
+                                Height = 100,
+                                Background = Brushes.Red,
+                            }),
+                            new Border()
                             {
-                                Content = new StackPanel()
+                                Name = "b2",
+                                Width = 100,
+                                Height = 100,
+                                Background = Brushes.Red,
+                                Margin = new Thickness(0, 100, 0, 0),
+                                Child = scroll = new ScrollContentPresenter()
                                 {
-                                    Children = new Controls.Controls
+                                    Content = new StackPanel()
                                     {
-                                        (item1 = new Border()
+                                        Children = new Controls.Controls
                                         {
-                                            Width = 100,
-                                            Height = 100,
-                                        }),
-                                        (item2 = new Border()
-                                        {
-                                            Width = 100,
-                                            Height = 100,
-                                        }),
+                                            (item1 = new Border()
+                                            {
+                                                Name = "b3",
+                                                Width = 100,
+                                                Height = 100,
+                                                Background = Brushes.Red,
+                                            }),
+                                            (item2 = new Border()
+                                            {
+                                                Name = "b4",
+                                                Width = 100,
+                                                Height = 100,
+                                                Background = Brushes.Red,
+                                            }),
+                                        }
                                     }
                                 }
                             }
@@ -341,11 +392,8 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
 
                 scroll.UpdateChild();
 
-                container.Measure(Size.Infinity);
-                container.Arrange(new Rect(container.DesiredSize));
-
-                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
-                context.Render(container);
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(container.DesiredSize));
 
                 var result = container.GetVisualsAt(new Point(50, 150)).First();
 
@@ -357,22 +405,96 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
 
                 scroll.Offset = new Vector(0, 100);
 
-                //we don't have setup LayoutManager so we will make it manually
+                // We don't have LayoutManager set up so do the layout pass manually.
                 scroll.Parent.InvalidateArrange();
                 container.InvalidateArrange();
-
                 container.Arrange(new Rect(container.DesiredSize));
-                context.Render(container);
 
                 result = container.GetVisualsAt(new Point(50, 150)).First();
-
                 Assert.Equal(item2, result);
 
                 result = container.GetVisualsAt(new Point(50, 50)).First();
-
-                Assert.NotEqual(item1, result);
                 Assert.Equal(target, result);
             }
         }
+
+        [Fact]
+        public void GetVisualsAt_Should_Not_Find_Path_When_Outside_Fill()
+        {
+            using (TestApplication())
+            {
+                Path path;
+                var container = new TestRoot
+                {
+                    Width = 200,
+                    Height = 200,
+                    Child = path = new Path
+                    {
+                        Width = 200,
+                        Height = 200,
+                        Fill = Brushes.Red,
+                        Data = StreamGeometry.Parse("M100,0 L0,100 100,100")
+                    }
+                };
+
+                container.Measure(Size.Infinity);
+                container.Arrange(new Rect(container.DesiredSize));
+
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
+
+                var result = container.GetVisualsAt(new Point(100, 100));
+                Assert.Equal(new[] { path }, result);
+
+                result = container.GetVisualsAt(new Point(10, 10));
+                Assert.Empty(result);
+            }
+        }
+
+        [Fact]
+        public void GetVisualsAt_Should_Respect_Geometry_Clip()
+        {
+            using (TestApplication())
+            {
+                Border border;
+                Canvas canvas;
+                var container = new TestRoot
+                {
+                    Width = 400,
+                    Height = 400,
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Red,
+                        Clip = StreamGeometry.Parse("M100,0 L0,100 100,100"),
+                        Width = 200,
+                        Height = 200,
+                        Child = canvas = new Canvas
+                        {
+                            Background = Brushes.Yellow,
+                            Margin = new Thickness(10),
+                        }
+                    }
+                };
+
+                container.Measure(Size.Infinity);
+                container.Arrange(new Rect(container.DesiredSize));
+                Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds);
+
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
+
+                var result = container.GetVisualsAt(new Point(200, 200));
+                Assert.Equal(new IVisual[] { canvas, border }, result);
+
+                result = container.GetVisualsAt(new Point(110, 110));
+                Assert.Empty(result);
+            }
+        }
+
+        private IDisposable TestApplication()
+        {
+            return UnitTestApplication.Start(
+                new TestServices(
+                    renderInterface: new MockPlatformRenderInterface(),
+                    renderer: (root, loop) => new ImmediateRenderer(root)));
+        }
     }
 }