Browse Source

WIP: Adding render layers.

Steven Kirk 9 years ago
parent
commit
d34c279ba1
27 changed files with 806 additions and 154 deletions
  1. 1 0
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 5 15
      src/Avalonia.Base/Threading/Dispatcher.cs
  3. 39 0
      src/Avalonia.Base/Threading/IDispatcher.cs
  4. 6 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  5. 28 0
      src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs
  6. 69 30
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  7. 11 0
      src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs
  8. 45 0
      src/Avalonia.Visuals/Rendering/LayerDirtyRects.cs
  9. 51 0
      src/Avalonia.Visuals/Rendering/RenderLayer.cs
  10. 61 0
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  11. 4 4
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  12. 10 0
      src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs
  13. 1 0
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  14. 58 20
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  15. 3 10
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  16. 0 1
      src/Avalonia.Visuals/Visual.cs
  17. 1 1
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  18. 9 15
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  19. 3 10
      src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
  20. 9 3
      src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs
  21. 1 0
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  22. 32 0
      tests/Avalonia.UnitTests/ImmediateDispatcher.cs
  23. 8 19
      tests/Avalonia.UnitTests/TestRoot.cs
  24. 1 0
      tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj
  25. 240 0
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  26. 9 2
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  27. 101 24
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

+ 1 - 0
src/Avalonia.Base/Avalonia.Base.csproj

@@ -111,6 +111,7 @@
     <Compile Include="Threading\Dispatcher.cs" />
     <Compile Include="Threading\DispatcherPriority.cs" />
     <Compile Include="Threading\DispatcherTimer.cs" />
+    <Compile Include="Threading\IDispatcher.cs" />
     <Compile Include="Threading\JobRunner.cs" />
     <Compile Include="Threading\AvaloniaScheduler.cs" />
     <Compile Include="Threading\AvaloniaSynchronizationContext.cs" />

+ 5 - 15
src/Avalonia.Base/Threading/Dispatcher.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Threading
     /// In Avalonia, there is usually only a single <see cref="Dispatcher"/> in the application -
     /// the one for the UI thread, retrieved via the <see cref="UIThread"/> property.
     /// </remarks>
-    public class Dispatcher
+    public class Dispatcher : IDispatcher
     {
         private readonly IPlatformThreadingInterface _platform;
         private readonly JobRunner _jobRunner;
@@ -33,15 +33,16 @@ namespace Avalonia.Threading
             _platform.Signaled += _jobRunner.RunJobs;
         }
 
+        /// <inheritdoc/>
         public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread ?? true;
 
+        /// <inheritdoc/>
         public void VerifyAccess()
         {
             if (!CheckAccess())
                 throw new InvalidOperationException("Call from invalid thread");
         }
 
-
         /// <summary>
         /// Runs the dispatcher's main loop.
         /// </summary>
@@ -63,24 +64,13 @@ namespace Avalonia.Threading
             _jobRunner?.RunJobs();
         }
 
-        /// <summary>
-        /// Invokes a method on the dispatcher thread.
-        /// </summary>
-        /// <param name="action">The method.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        /// <returns>A task that can be used to track the method's execution.</returns>
+        /// <inheritdoc/>
         public Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
         {
             return _jobRunner?.InvokeAsync(action, priority);
         }
 
-        /// <summary>
-        /// Post action that will be invoked on main thread
-        /// </summary>
-        /// <param name="action">The method.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        // TODO: The naming of this method is confusing: the Async suffix usually means return a task.
-        // Remove this and rename InvokeTaskAsync as InvokeAsync.
+        /// <inheritdoc/>
         public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
         {
             _jobRunner?.Post(action, priority);

+ 39 - 0
src/Avalonia.Base/Threading/IDispatcher.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Avalonia.Threading
+{
+    /// <summary>
+    /// Dispatches jobs to a thread.
+    /// </summary>
+    public interface IDispatcher
+    {
+        /// <summary>
+        /// Determines whether the calling thread is the thread associated with this <see cref="IDispatcher"/>.
+        /// </summary>
+        /// <returns>True if he calling thread is the thread associated with the dispatched, otherwise false.</returns>
+        bool CheckAccess();
+
+        /// <summary>
+        /// Throws an exception if the calling thread is not the thread associated with this <see cref="IDispatcher"/>.
+        /// </summary>
+        void VerifyAccess();
+
+        /// <summary>
+        /// Invokes a method on the dispatcher thread.
+        /// </summary>
+        /// <param name="action">The method.</param>
+        /// <param name="priority">The priority with which to invoke the method.</param>
+        /// <returns>A task that can be used to track the method's execution.</returns>
+        void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal);
+
+        /// <summary>
+        /// Post action that will be invoked on main thread
+        /// </summary>
+        /// <param name="action">The method.</param>
+        /// <param name="priority">The priority with which to invoke the method.</param>
+        // TODO: The naming of this method is confusing: the Async suffix usually means return a task.
+        // Remove this and rename InvokeTaskAsync as InvokeAsync. See #816.
+        Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal);
+    }
+}

+ 6 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -106,18 +106,24 @@
     <Compile Include="Rect.cs" />
     <Compile Include="RelativePoint.cs" />
     <Compile Include="RelativeRect.cs" />
+    <Compile Include="Rendering\DefaultRenderLayerFactory.cs" />
     <Compile Include="Rendering\DeferredRenderer.cs" />
     <Compile Include="Rendering\IRenderer.cs" />
     <Compile Include="Rendering\IRendererFactory.cs" />
     <Compile Include="Rendering\IRenderLoop.cs" />
     <Compile Include="Rendering\DirtyRects.cs" />
+    <Compile Include="Rendering\LayerDirtyRects.cs" />
     <Compile Include="Rendering\Renderer.cs" />
     <Compile Include="Rendering\RendererMixin.cs" />
     <Compile Include="Rendering\DefaultRenderLoop.cs" />
+    <Compile Include="Rendering\IRenderLayerFactory.cs" />
+    <Compile Include="Rendering\RenderLayer.cs" />
+    <Compile Include="Rendering\RenderLayers.cs" />
     <Compile Include="Rendering\SceneGraph\DeferredDrawingContextImpl.cs" />
     <Compile Include="Rendering\SceneGraph\GeometryNode.cs" />
     <Compile Include="Rendering\SceneGraph\IDrawOperation.cs" />
     <Compile Include="Rendering\SceneGraph\ImageNode.cs" />
+    <Compile Include="Rendering\SceneGraph\ISceneBuilder.cs" />
     <Compile Include="Rendering\SceneGraph\IVisualNode.cs" />
     <Compile Include="Rendering\SceneGraph\LineNode.cs" />
     <Compile Include="Rendering\SceneGraph\RectangleNode.cs" />

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

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

+ 69 - 30
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -11,12 +11,15 @@ namespace Avalonia.Rendering
 {
     public class DeferredRenderer : IRenderer
     {
+        private readonly IDispatcher _dispatcher;
         private readonly IRenderLoop _renderLoop;
         private readonly IRenderRoot _root;
+        private readonly ISceneBuilder _sceneBuilder;
+        private readonly RenderLayers _layers;
         private Scene _scene;
         private IRenderTarget _renderTarget;
         private List<IVisual> _dirty;
-        private DirtyRects _dirtyRects;
+        private LayerDirtyRects _dirtyRects;
         private bool _updateQueued;
         private bool _rendering;
 
@@ -26,12 +29,20 @@ namespace Avalonia.Rendering
         private int _fps;
         private TimeSpan _lastFpsUpdate;
 
-        public DeferredRenderer(IRenderRoot root, IRenderLoop renderLoop)
+        public DeferredRenderer(
+            IRenderRoot root,
+            IRenderLoop renderLoop,
+            ISceneBuilder sceneBuilder = null,
+            IRenderLayerFactory layerFactory = null,
+            IDispatcher dispatcher = null)
         {
             Contract.Requires<ArgumentNullException>(root != null);
 
+            _dispatcher = dispatcher ?? Dispatcher.UIThread;
             _root = root;
+            _sceneBuilder = sceneBuilder ?? new SceneBuilder();
             _scene = new Scene(root);
+            _layers = new RenderLayers(layerFactory ?? new DefaultRenderLayerFactory());
 
             if (renderLoop != null)
             {
@@ -70,25 +81,28 @@ namespace Avalonia.Rendering
         {
         }
 
-        private void Render(IDrawingContextImpl context, IVisualNode node, Rect clipBounds)
+        private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds)
         {
-            clipBounds = node.ClipBounds.Intersect(clipBounds);
-
-            if (!clipBounds.IsEmpty)
+            if (node.LayerRoot == layer)
             {
-                node.BeginRender(context);
+                clipBounds = node.ClipBounds.Intersect(clipBounds);
 
-                foreach (var operation in node.DrawOperations)
+                if (!clipBounds.IsEmpty)
                 {
-                    operation.Render(context);
-                }
+                    node.BeginRender(context);
 
-                foreach (var child in node.Children)
-                {
-                    Render(context, child, clipBounds);
-                }
+                    foreach (var operation in node.DrawOperations)
+                    {
+                        operation.Render(context);
+                    }
+
+                    foreach (var child in node.Children)
+                    {
+                        Render(context, (VisualNode)child, layer, clipBounds);
+                    }
 
-                node.EndRender(context);
+                    node.EndRender(context);
+                }
             }
         }
 
@@ -125,19 +139,18 @@ namespace Avalonia.Rendering
             try
             {
                 var scene = _scene.Clone();
-                var dirtyRects = new DirtyRects();
+                var dirtyRects = new LayerDirtyRects();
 
                 if (_dirty == null)
                 {
                     _dirty = new List<IVisual>();
-                    SceneBuilder.UpdateAll(scene);
-                    dirtyRects.Add(new Rect(_root.ClientSize));
+                    _sceneBuilder.UpdateAll(scene, dirtyRects);
                 }
                 else if (_dirty.Count > 0)
                 {
                     foreach (var visual in _dirty)
                     {
-                        SceneBuilder.Update(scene, visual, dirtyRects);
+                        _sceneBuilder.Update(scene, visual, dirtyRects);
                     }
 
                     dirtyRects.Coalesce();
@@ -167,14 +180,16 @@ namespace Avalonia.Rendering
 
             if (!_updateQueued && (_dirty == null || _dirty.Count > 0 || _dirtyRects != null))
             {
-                Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
                 _updateQueued = true;
+                _dispatcher.InvokeAsync(UpdateScene, DispatcherPriority.Render);
             }
 
             _rendering = true;
+            _totalFrames++;
 
             Scene scene;
-            DirtyRects dirtyRects;
+            LayerDirtyRects dirtyRects;
+            int updateCount = 0;
 
             lock (_scene)
             {
@@ -182,6 +197,30 @@ namespace Avalonia.Rendering
                 dirtyRects = _dirtyRects;
             }
 
+            var toRemove = new List<IVisual>();
+
+            if (dirtyRects != null)
+            {
+                foreach (var layer in dirtyRects)
+                {
+                    var renderTarget = GetRenderTargetForLayer(layer.Key);
+                    var node = (VisualNode)scene.FindNode(layer.Key);
+
+                    using (var context = renderTarget.CreateDrawingContext())
+                    {
+                        foreach (var rect in layer.Value)
+                        {
+                            context.PushClip(rect);
+                            Render(context, node, layer.Key, rect);
+                            context.PopClip();
+                            ++updateCount;
+                        }
+                    }
+                }
+
+                _layers.RemoveUnused(scene);
+            }
+
             try
             {
                 if (_renderTarget == null)
@@ -191,18 +230,13 @@ namespace Avalonia.Rendering
 
                 using (var context = _renderTarget.CreateDrawingContext())
                 {
-                    int updateCount = 0;
-
-                    _totalFrames++;
-
                     if (dirtyRects != null)
                     {
-                        foreach (var rect in dirtyRects)
+                        var rect = new Rect(_root.ClientSize);
+
+                        foreach (var layer in _layers)
                         {
-                            context.PushClip(rect);
-                            Render(context, _scene.Root, rect);
-                            context.PopClip();
-                            ++updateCount;
+                            context.DrawImage(layer.Bitmap, layer.LayerRoot.Opacity, rect, rect);
                         }
                     }
 
@@ -221,5 +255,10 @@ namespace Avalonia.Rendering
 
             _rendering = false;
         }
+
+        private IRenderTargetBitmapImpl GetRenderTargetForLayer(IVisual layerRoot)
+        {
+            return (_layers.Get(layerRoot) ?? _layers.Add(layerRoot, _root.ClientSize)).Bitmap;
+        }
     }
 }

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

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

+ 45 - 0
src/Avalonia.Visuals/Rendering/LayerDirtyRects.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class LayerDirtyRects : Dictionary<IVisual, DirtyRects>
+    {
+        public bool IsEmpty
+        {
+            get
+            {
+                foreach (var i in Values)
+                {
+                    if (!i.IsEmpty)
+                    {
+                        return false;
+                    }
+                }
+
+                return true;
+            }
+        }
+
+        public void Add(IVisual layerRoot, Rect rect)
+        {
+            DirtyRects rects;
+
+            if (!TryGetValue(layerRoot, out rects))
+            {
+                Add(layerRoot, rects = new DirtyRects());
+            }
+
+            rects.Add(rect);
+        }
+
+        public void Coalesce()
+        {
+            foreach (var i in Values)
+            {
+                i.Coalesce();
+            }
+        }
+    }
+}

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

@@ -0,0 +1,51 @@
+using System;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class RenderLayer : IComparable<RenderLayer>
+    {
+        public RenderLayer(
+            IRenderTargetBitmapImpl bitmap,
+            Size size,
+            IVisual layerRoot)
+        {
+            Bitmap = bitmap;
+            Size = size;
+            LayerRoot = layerRoot;
+            Order = GetDistanceFromRenderRoot(layerRoot);
+        }
+
+        public IRenderTargetBitmapImpl Bitmap { get; }
+        public Size Size { get; }
+        public IVisual LayerRoot { get; }
+        public int Order { get; }
+
+        private static int GetDistanceFromRenderRoot(IVisual visual)
+        {
+            var root = visual as IRenderRoot;
+            var result = 0;
+
+            while (root == null)
+            {
+                ++result;
+                visual = visual.VisualParent;
+
+                if (visual == null)
+                {
+                    throw new AvaloniaInternalException("Visual is not rooted.");
+                }
+
+                root = visual as IRenderRoot;
+            }
+
+            return result;
+        }
+
+        public int CompareTo(RenderLayer other)
+        {
+            return Order - other.Order;
+        }
+    }
+}

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

@@ -0,0 +1,61 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering
+{
+    public class RenderLayers : IEnumerable<RenderLayer>
+    {
+        private readonly IRenderLayerFactory _factory;
+        private List<RenderLayer> _inner = new List<RenderLayer>();
+        private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
+
+        public RenderLayers(IRenderLayerFactory factory)
+        {
+            _factory = factory;
+        }
+
+        public RenderLayer Add(IVisual layerRoot, Size size)
+        {
+            RenderLayer result;
+
+            if (!_index.TryGetValue(layerRoot, out result))
+            {
+                var bitmap = _factory.CreateLayer(layerRoot, size);
+                result = new RenderLayer(bitmap, size, layerRoot);
+                _inner.Add(result);
+                _index.Add(layerRoot, result);
+            }
+
+            return result;
+        }
+
+        public RenderLayer Get(IVisual layerRoot)
+        {
+            RenderLayer result;
+            _index.TryGetValue(layerRoot, out result);
+            return result;
+        }
+
+        public void RemoveUnused(Scene scene)
+        {
+            for (var i = _inner.Count - 1; i >= 0; --i)
+            {
+                var layer = _inner[i];
+                var node = (VisualNode)scene.FindNode(layer.LayerRoot);
+
+                if (node == null || node.LayerRoot != layer.LayerRoot)
+                {
+                    layer.Bitmap.Dispose();
+                    _inner.RemoveAt(i);
+                    _index.Remove(layer.LayerRoot);
+                }
+            }
+        }
+
+        public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+    }
+}

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

@@ -14,18 +14,18 @@ namespace Avalonia.Rendering.SceneGraph
         private int _drawOperationindex;
 
         public DeferredDrawingContextImpl()
-            : this(new DirtyRects())
+            : this(new LayerDirtyRects())
         {
         }
 
-        public DeferredDrawingContextImpl(DirtyRects dirty)
+        public DeferredDrawingContextImpl(LayerDirtyRects dirty)
         {
             Dirty = dirty;
         }
 
         public Matrix Transform { get; set; } = Matrix.Identity;
 
-        public DirtyRects Dirty { get; }
+        public LayerDirtyRects Dirty { get; }
 
         public UpdateState BeginUpdate(VisualNode node)
         {
@@ -205,7 +205,7 @@ namespace Avalonia.Rendering.SceneGraph
 
                 foreach (var operation in Owner._node.DrawOperations)
                 {
-                    Owner.Dirty.Add(operation.Bounds);
+                    Owner.Dirty.Add(Owner._node.LayerRoot, operation.Bounds);
                 }
 
                 Owner._node = Node;

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

@@ -0,0 +1,10 @@
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    public interface ISceneBuilder
+    {
+        bool Update(Scene scene, IVisual visual, LayerDirtyRects dirty);
+        void UpdateAll(Scene scene, LayerDirtyRects dirty);
+    }
+}

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

@@ -23,6 +23,7 @@ namespace Avalonia.Rendering.SceneGraph
 
             _index = index;
             Root = root;
+            root.LayerRoot = root.Visual;
         }
 
         public IVisualNode Root { get; }

+ 58 - 20
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -6,31 +6,24 @@ using System.Linq;
 using Avalonia.Media;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
-using System.Collections.Generic;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    public static class SceneBuilder
+    public class SceneBuilder : ISceneBuilder
     {
-        public static void UpdateAll(Scene scene)
+        public void UpdateAll(Scene scene, LayerDirtyRects dirty)
         {
             Contract.Requires<ArgumentNullException>(scene != null);
             Dispatcher.UIThread.VerifyAccess();
 
-            using (var impl = new DeferredDrawingContextImpl())
+            using (var impl = new DeferredDrawingContextImpl(dirty))
             using (var context = new DrawingContext(impl))
             {
                 Update(context, scene, (VisualNode)scene.Root, scene.Root.Visual.Bounds, true);
             }
         }
 
-        public static bool Update(Scene scene, IVisual visual)
-        {
-            var dirty = new DirtyRects();
-            return Update(scene, visual, dirty);
-        }
-
-        public static bool Update(Scene scene, IVisual visual, DirtyRects dirty)
+        public bool Update(Scene scene, IVisual visual, LayerDirtyRects dirty)
         {
             Contract.Requires<ArgumentNullException>(scene != null);
             Contract.Requires<ArgumentNullException>(visual != null);
@@ -131,7 +124,7 @@ namespace Avalonia.Rendering.SceneGraph
             var bounds = new Rect(visual.Bounds.Size);
             var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
 
-            contextImpl.Dirty.Add(node.Bounds);
+            contextImpl.Dirty.Add(node.LayerRoot, node.Bounds);
 
             if (visual.IsVisible)
             {
@@ -161,6 +154,15 @@ namespace Avalonia.Rendering.SceneGraph
                     node.Opacity = opacity;
                     node.OpacityMask = visual.OpacityMask;
 
+                    if (opacity < 1)
+                    {
+                        SetLayer(node, node.Visual);
+                    }
+                    else if (node.LayerRoot == node.Visual && node.Parent != null)
+                    {
+                        ClearLayer(node, contextImpl.Dirty);
+                    }
+
                     if (node.ClipToBounds)
                     {
                         clip = clip.Intersect(node.ClipBounds);
@@ -191,26 +193,27 @@ namespace Avalonia.Rendering.SceneGraph
             }
         }
 
-        private static VisualNode CreateNode(Scene scene, IVisual visual, IVisualNode parent)
+        private static VisualNode CreateNode(Scene scene, IVisual visual, VisualNode parent)
         {
             var node = new VisualNode(visual, parent);
+            node.LayerRoot = parent.LayerRoot;
             scene.Add(node);
             return node;
         }
 
-        private static void Deindex(Scene scene, VisualNode node, DirtyRects dirty)
+        private static void Deindex(Scene scene, VisualNode node, LayerDirtyRects dirty)
         {
             scene.Remove(node);
             node.SubTreeUpdated = true;
 
-            foreach (var child in node.Children)
+            foreach (VisualNode child in node.Children)
             {
                 var geometry = child as IDrawOperation;
                 var visual = child as VisualNode;
 
                 if (geometry != null)
                 {
-                    dirty.Add(geometry.Bounds);
+                    dirty.Add(child.LayerRoot, geometry.Bounds);
                 }
 
                 if (visual != null)
@@ -220,13 +223,48 @@ namespace Avalonia.Rendering.SceneGraph
             }
         }
 
-        private static void AddSubtreeBounds(VisualNode node, DirtyRects dirty)
+        private static void AddSubtreeBounds(VisualNode node, LayerDirtyRects dirty)
         {
-            dirty.Add(node.Bounds);
+            dirty.Add(node.LayerRoot, node.Bounds);
 
-            foreach (var child in node.Children)
+            foreach (VisualNode child in node.Children)
             {
-                AddSubtreeBounds((VisualNode)child, dirty);
+                if (child.LayerRoot == node.LayerRoot)
+                {
+                    AddSubtreeBounds(child, dirty);
+                }
+            }
+        }
+
+        private static void ClearLayer(VisualNode node, LayerDirtyRects dirty)
+        {
+            var parent = (VisualNode)node.Parent;
+            var newLayerRoot = parent.LayerRoot;
+            var existingDirtyRects = dirty[node.LayerRoot];
+
+            existingDirtyRects.Coalesce();
+
+            foreach (var r in existingDirtyRects)
+            {
+                dirty.Add(newLayerRoot, r);
+            }
+
+            dirty.Remove(node.LayerRoot);
+
+            SetLayer(node, newLayerRoot);
+        }
+
+        private static void SetLayer(VisualNode node, IVisual layerRoot)
+        {
+            node.LayerRoot = layerRoot;
+
+            foreach (VisualNode child in node.Children)
+            {
+                // If the child is not the start of a new layer, recurse.
+                if (child.LayerRoot != child.Visual)
+                {
+                    SetLayer(child, layerRoot);
+                }
             }
         }
     }

+ 3 - 10
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -94,6 +94,8 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         public bool OpacityChanged { get; private set; }
 
+        public IVisual LayerRoot { get; set; }
+
         /// <inheritdoc/>
         public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren;
 
@@ -197,6 +199,7 @@ namespace Avalonia.Rendering.SceneGraph
                 OpacityMask = OpacityMask,
                 _drawOperations = _drawOperations,
                 _drawOperationsCloned = true,
+                LayerRoot= LayerRoot,
             };
         }
 
@@ -219,11 +222,6 @@ namespace Avalonia.Rendering.SceneGraph
         {
             context.Transform = Transform;
 
-            if (Opacity != 1)
-            {
-                context.PushOpacity(Opacity);
-            }
-
             if (ClipToBounds)
             {
                 context.PushClip(ClipBounds * Transform.Invert());
@@ -237,11 +235,6 @@ namespace Avalonia.Rendering.SceneGraph
             {
                 context.PopClip();
             }
-
-            if (Opacity != 1)
-            {
-                context.PopOpacity();
-            }
         }
 
         private Rect CalculateBounds()

+ 0 - 1
src/Avalonia.Visuals/Visual.cs

@@ -180,7 +180,6 @@ namespace Avalonia
             set { SetValue(OpacityProperty, value); }
         }
 
-
         /// <summary>
         /// Gets the opacity mask of the scene graph node.
         /// </summary>

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

@@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1
 
         public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
         {
-            return new RenderTargetBitmapImpl(s_imagingFactory, s_d2D1Factory, width, height);
+            return new RenderTargetBitmapImpl(s_imagingFactory, s_d2D1Factory, s_dwfactory, width, height);
         }
 
         public IStreamGeometryImpl CreateStreamGeometry()

+ 9 - 15
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -18,14 +18,7 @@ namespace Avalonia.Direct2D1.Media
     /// </summary>
     public class DrawingContextImpl : IDrawingContextImpl, IDisposable
     {
-        /// <summary>
-        /// The Direct2D1 render target.
-        /// </summary>
         private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
-
-        /// <summary>
-        /// The DirectWrite factory.
-        /// </summary>
         private SharpDX.DirectWrite.Factory _directWriteFactory;
 
         /// <summary>
@@ -77,14 +70,15 @@ 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)
         {
-            BitmapImpl 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>

+ 3 - 10
src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs

@@ -15,8 +15,6 @@ namespace Avalonia.Direct2D1.Media
     {
         private readonly ImagingFactory _factory;
 
-        private SharpDX.Direct2D1.Bitmap _direct2D;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="BitmapImpl"/> class.
         /// </summary>
@@ -93,14 +91,9 @@ namespace Avalonia.Direct2D1.Media
         /// <returns>The Direct2D bitmap.</returns>
         public SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget)
         {
-            if (_direct2D == null)
-            {
-                FormatConverter converter = new FormatConverter(_factory);
-                converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA);
-                _direct2D = SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
-            }
-
-            return _direct2D;
+            FormatConverter converter = new FormatConverter(_factory);
+            converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA);
+            return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
         }
 
         /// <summary>

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

@@ -4,19 +4,21 @@
 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 : BitmapImpl, IRenderTargetBitmapImpl, IDisposable
     {
+        private readonly DirectWriteFactory _dwriteFactory;
         private readonly WicRenderTarget _target;
 
         public RenderTargetBitmapImpl(
             ImagingFactory imagingFactory,
             Factory d2dFactory,
+            DirectWriteFactory dwriteFactory,
             int width,
             int height)
             : base(imagingFactory, width, height)
@@ -31,6 +33,8 @@ namespace Avalonia.Direct2D1.Media
                 d2dFactory,
                 WicImpl,
                 props);
+
+            _dwriteFactory = dwriteFactory;
         }
 
         public override void Dispose()
@@ -39,7 +43,9 @@ namespace Avalonia.Direct2D1.Media
             base.Dispose();
         }
 
-        public IDrawingContextImpl CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext();
-        
+        public IDrawingContextImpl CreateDrawingContext()
+        {
+            return new DrawingContextImpl(_target, _dwriteFactory);
+        }        
     }
 }

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

@@ -54,6 +54,7 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="ImmediateDispatcher.cs" />
     <Compile Include="InvariantCultureFixture.cs" />
     <Compile Include="NotifyingBase.cs" />
     <Compile Include="TestLogSink.cs" />

+ 32 - 0
tests/Avalonia.UnitTests/ImmediateDispatcher.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+
+namespace Avalonia.UnitTests
+{
+    /// <summary>
+    /// Immediately invokes dispatched jobs on the current thread.
+    /// </summary>
+    public class ImmediateDispatcher : IDispatcher
+    {
+        public bool CheckAccess()
+        {
+            return true;
+        }
+
+        public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
+        {
+            action();
+        }
+
+        public Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
+        {
+            action();
+            return Task.FromResult<object>(null);
+        }
+
+        public void VerifyAccess()
+        {
+        }
+    }
+}

+ 8 - 19
tests/Avalonia.UnitTests/TestRoot.cs

@@ -5,15 +5,19 @@ using System;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Layout;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Styling;
+using Moq;
 
 namespace Avalonia.UnitTests
 {
     public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, INameScope, IRenderRoot, IStyleRoot
     {
         private readonly NameScope _nameScope = new NameScope();
+        private readonly IRenderTarget _renderTarget = Mock.Of<IRenderTarget>(
+            x => x.CreateDrawingContext() == Mock.Of<IDrawingContextImpl>());
 
         public TestRoot()
         {
@@ -48,32 +52,17 @@ namespace Avalonia.UnitTests
 
         public IRenderTarget RenderTarget => null;
 
-        public IRenderer Renderer { get; }
+        public IRenderer Renderer { get; set; }
 
-        public IAccessKeyHandler AccessKeyHandler
-        {
-            get
-            {
-                throw new NotImplementedException();
-            }
-        }
+        public IAccessKeyHandler AccessKeyHandler => null;
 
-        public IKeyboardNavigationHandler KeyboardNavigationHandler
-        {
-            get
-            {
-                throw new NotImplementedException();
-            }
-        }
+        public IKeyboardNavigationHandler KeyboardNavigationHandler => null;
 
         public IInputElement PointerOverElement { get; set; }
 
         public bool ShowAccessKeys { get; set; }
 
-        public IRenderTarget CreateRenderTarget()
-        {
-            throw new NotImplementedException();
-        }
+        public IRenderTarget CreateRenderTarget() => _renderTarget;
 
         public void Invalidate(Rect rect)
         {

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

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

+ 240 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Rendering
+{
+    public class DeferredRendererTests
+    {
+        [Fact]
+        public void First_Frame_Calls_UpdateScene_On_Dispatcher()
+        {
+            var loop = new Mock<IRenderLoop>();
+            var root = new TestRoot();
+            var dispatcher = new Mock<IDispatcher>();
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: null,
+                dispatcher: dispatcher.Object);
+
+            RunFrame(loop);
+
+            dispatcher.Verify(x => 
+                x.InvokeAsync(
+                    It.Is<Action>(a => a.Method.Name == "UpdateScene"),
+                    DispatcherPriority.Render));
+        }
+
+        [Fact]
+        public void First_Frame_Calls_SceneBuilder_UpdateAll()
+        {
+            var loop = new Mock<IRenderLoop>();
+            var root = new TestRoot();
+            var sceneBuilder = new Mock<ISceneBuilder>();
+            var dispatcher = new ImmediateDispatcher();
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: sceneBuilder.Object,
+                dispatcher: dispatcher);
+
+            RunFrame(loop);
+
+            sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>(), It.IsAny<LayerDirtyRects>()));
+        }
+
+        [Fact]
+        public void Frame_Does_Not_Call_SceneBuilder_If_No_Dirty_Controls()
+        {
+            var loop = new Mock<IRenderLoop>();
+            var root = new TestRoot();
+            var sceneBuilder = new Mock<ISceneBuilder>();
+            var dispatcher = new ImmediateDispatcher();
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: sceneBuilder.Object,
+                dispatcher: dispatcher);
+
+            IgnoreFirstFrame(loop, sceneBuilder);
+            RunFrame(loop);
+
+            sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>(), It.IsAny<LayerDirtyRects>()), Times.Never);
+            sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), It.IsAny<Visual>(), It.IsAny<LayerDirtyRects>()), Times.Never);
+        }
+
+        [Fact]
+        public void Frame_Should_Call_SceneBuilder_Update_With_Dirty_Controls()
+        {
+            var loop = new Mock<IRenderLoop>();
+            var root = new TestRoot();
+            var sceneBuilder = new Mock<ISceneBuilder>();
+            var dispatcher = new ImmediateDispatcher();
+            var control1 = new Border();
+            var control2 = new Canvas();
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: sceneBuilder.Object,
+                dispatcher: dispatcher);
+
+            IgnoreFirstFrame(loop, sceneBuilder);
+            target.AddDirty(control1);
+            target.AddDirty(control2);
+            RunFrame(loop);
+
+            sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), control1, It.IsAny<LayerDirtyRects>()));
+            sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), control2, It.IsAny<LayerDirtyRects>()));
+        }
+
+        [Fact]
+        public void Frame_Should_Create_Layer_For_Root()
+        {
+            var loop = new Mock<IRenderLoop>();
+            var root = new TestRoot();
+            var rootLayer = new Mock<IRenderTargetBitmapImpl>();
+            var dispatcher = new ImmediateDispatcher();
+
+            var sceneBuilder = new Mock<ISceneBuilder>();
+            sceneBuilder.Setup(x => x.UpdateAll(It.IsAny<Scene>(), It.IsAny<LayerDirtyRects>()))
+                .Callback<Scene, LayerDirtyRects>((scene, dirty) =>
+                {
+                    var rects = new DirtyRects();
+                    rects.Add(new Rect(root.ClientSize));
+                    dirty.Add(root, rects);
+                });
+
+            var layers = new Mock<IRenderLayerFactory>();
+            layers.Setup(x => x.CreateLayer(root, root.ClientSize)).Returns(CreateLayer());
+
+            var renderInterface = new Mock<IPlatformRenderInterface>();
+
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: sceneBuilder.Object,
+                layerFactory: layers.Object,
+                dispatcher: dispatcher);
+
+            RunFrame(loop);
+
+            layers.Verify(x => x.CreateLayer(root, root.ClientSize));
+        }
+
+        [Fact]
+        public void Should_Create_And_Delete_Layers_For_Transparent_Controls()
+        {
+            Border border;
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = new Border
+                {
+                    Background = Brushes.Red,
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Green,
+                    }
+                }
+            };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
+
+            var loop = new Mock<IRenderLoop>();
+            var layerFactory = new MockRenderLayerFactory(new Dictionary<IVisual, IRenderTargetBitmapImpl>
+            {
+                { root, CreateLayer() },
+                { border, CreateLayer() },
+            });
+
+            var target = new DeferredRenderer(
+                root, 
+                loop.Object,
+                layerFactory: layerFactory,
+                dispatcher: new ImmediateDispatcher());
+            root.Renderer = target;
+
+            RunFrame(loop);
+
+            var rootContext = layerFactory.GetMockDrawingContext(root);
+            var borderContext = layerFactory.GetMockDrawingContext(border);
+
+            rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+            rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
+            borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never);
+
+            rootContext.ResetCalls();
+            borderContext.ResetCalls();
+            border.Opacity = 0.5;
+            RunFrame(loop);
+
+            rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+            rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Never);
+            borderContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
+
+            rootContext.ResetCalls();
+            borderContext.ResetCalls();
+            border.Opacity = 1;
+            RunFrame(loop);
+
+            layerFactory.GetMockBitmap(border).Verify(x => x.Dispose());
+            rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+            rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
+            borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never);
+        }
+
+        private void IgnoreFirstFrame(Mock<IRenderLoop> loop, Mock<ISceneBuilder> sceneBuilder)
+        {
+            RunFrame(loop);
+            sceneBuilder.ResetCalls();
+        }
+
+        private void RunFrame(Mock<IRenderLoop> loop)
+        {
+            loop.Raise(x => x.Tick += null, EventArgs.Empty);
+        }
+
+        private IRenderTargetBitmapImpl CreateLayer()
+        {
+            return Mock.Of<IRenderTargetBitmapImpl>(x =>
+                x.CreateDrawingContext() == Mock.Of<IDrawingContextImpl>());
+        }
+
+        private class MockRenderLayerFactory : IRenderLayerFactory
+        {
+            private IDictionary<IVisual, IRenderTargetBitmapImpl> _layers;
+
+            public MockRenderLayerFactory(IDictionary<IVisual, IRenderTargetBitmapImpl> layers)
+            {
+                _layers = layers;
+            }
+
+            public IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size)
+            {
+                return _layers[layerRoot];
+            }
+
+            public Mock<IRenderTargetBitmapImpl> GetMockBitmap(IVisual layerRoot)
+            {
+                return Mock.Get(_layers[layerRoot]);
+            }
+
+            public Mock<IDrawingContextImpl> GetMockDrawingContext(IVisual layerRoot)
+            {
+                return Mock.Get(_layers[layerRoot].CreateDrawingContext());
+            }
+        }
+    }
+}

+ 9 - 2
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs

@@ -88,6 +88,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             var node = new VisualNode(Mock.Of<IVisual>(), null);
             var target = new DeferredDrawingContextImpl();
 
+            node.LayerRoot = node.Visual;
+
             using (target.BeginUpdate(node))
             {
                 target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
@@ -106,6 +108,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
             var target = new DeferredDrawingContextImpl();
 
+            node.LayerRoot = node.Visual;
             node.AddDrawOperation(operation);
 
             using (target.BeginUpdate(node))
@@ -126,6 +129,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
             var target = new DeferredDrawingContextImpl();
 
+            node.LayerRoot = node.Visual;
             node.AddDrawOperation(operation);
 
             using (target.BeginUpdate(node))
@@ -144,15 +148,17 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         {
             var node = new VisualNode(Mock.Of<IVisual>(), null);
             var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
-            var dirtyRects = new DirtyRects();
+            var dirtyRects = new LayerDirtyRects();
             var target = new DeferredDrawingContextImpl(dirtyRects);
 
+            node.LayerRoot = node.Visual;
+
             using (target.BeginUpdate(node))
             {
                 target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
             }
 
-            Assert.Equal(new Rect(0, 0, 100, 100), dirtyRects.Single());
+            Assert.Equal(new Rect(0, 0, 100, 100), dirtyRects.Single().Value.Single());
         }
 
         [Fact]
@@ -160,6 +166,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         {
             var node = new VisualNode(Mock.Of<IVisual>(), null);
 
+            node.LayerRoot = node.Visual;
             node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 10, 100), 0));
             node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 20, 100), 0));
             node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 30, 100), 0));

+ 101 - 24
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -38,8 +38,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var result = new Scene(tree);
-                SceneBuilder.UpdateAll(result);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(result, new LayerDirtyRects());
 
+                Assert.Same(tree, ((VisualNode)result.Root).LayerRoot);
                 Assert.Equal(1, result.Root.Children.Count);
 
                 var borderNode = (VisualNode)result.Root.Children[0];
@@ -85,14 +87,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var result = new Scene(tree);
-                SceneBuilder.UpdateAll(result);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(result, new LayerDirtyRects());
 
                 var canvasNode = result.FindNode(canvas);
                 Assert.Equal(new Rect(10, 20, 160, 240), canvasNode.ClipBounds);
 
                 // Initial ClipBounds are correct, make sure they're still correct after updating canvas.
                 result = result.Clone();
-                Assert.True(SceneBuilder.Update(result, canvas));
+                Assert.True(sceneBuilder.Update(result, canvas, new LayerDirtyRects()));
 
                 canvasNode = result.FindNode(canvas);
                 Assert.Equal(new Rect(10, 20, 160, 240), canvasNode.ClipBounds);
@@ -134,14 +137,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var scene = new Scene(tree);
-                SceneBuilder.UpdateAll(scene);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(scene, new LayerDirtyRects());
 
                 var borderNode = scene.FindNode(border);
                 Assert.Equal(new Rect(50, 50, 50, 50), borderNode.ClipBounds);
 
                 // Initial ClipBounds are correct, make sure they're still correct after updating border.
                 scene = scene.Clone();
-                Assert.True(SceneBuilder.Update(scene, border));
+                Assert.True(sceneBuilder.Update(scene, border, new LayerDirtyRects()));
 
                 borderNode = scene.FindNode(border);
                 Assert.Equal(new Rect(50, 50, 50, 50), borderNode.ClipBounds);
@@ -174,7 +178,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 };
 
                 var result = new Scene(tree);
-                SceneBuilder.UpdateAll(result);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(result, new LayerDirtyRects());
 
                 var panelNode = result.FindNode(tree.Child);
                 var expected = new IVisual[] { back, front };
@@ -207,7 +212,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var result = new Scene(tree);
-                SceneBuilder.UpdateAll(result);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(result, new LayerDirtyRects());
 
                 var targetNode = result.FindNode(target);
 
@@ -241,7 +247,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var initial = new Scene(tree);
-                SceneBuilder.UpdateAll(initial);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(initial, new LayerDirtyRects());
 
                 var initialBackgroundNode = initial.FindNode(border).Children[0];
                 var initialTextNode = initial.FindNode(textBlock).DrawOperations[0];
@@ -252,7 +259,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 border.Background = Brushes.Green;
 
                 var result = initial.Clone();
-                SceneBuilder.Update(result, border);
+                sceneBuilder.Update(result, border, new LayerDirtyRects());
                 
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Same(border, borderNode.Visual);
@@ -295,16 +302,17 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var initial = new Scene(tree);
-                SceneBuilder.UpdateAll(initial);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(initial, new LayerDirtyRects());
 
                 border.Child = decorator;
                 var result = initial.Clone();
 
-                Assert.True(SceneBuilder.Update(result, decorator));
+                Assert.True(sceneBuilder.Update(result, decorator, new LayerDirtyRects()));
 
                 // Updating canvas should result in no-op as it should have been updated along 
                 // with decorator as part of the add opeation.
-                Assert.False(SceneBuilder.Update(result, canvas));
+                Assert.False(sceneBuilder.Update(result, canvas, new LayerDirtyRects()));
 
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Equal(1, borderNode.Children.Count);
@@ -349,13 +357,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var initial = new Scene(tree);
-                SceneBuilder.UpdateAll(initial);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(initial, new LayerDirtyRects());
 
                 border.Child = null;
                 var result = initial.Clone();
 
-                Assert.True(SceneBuilder.Update(result, decorator));
-                Assert.False(SceneBuilder.Update(result, canvas));
+                Assert.True(sceneBuilder.Update(result, decorator, new LayerDirtyRects()));
+                Assert.False(sceneBuilder.Update(result, canvas, new LayerDirtyRects()));
 
                 var borderNode = (VisualNode)result.Root.Children[0];
                 Assert.Equal(0, borderNode.Children.Count);
@@ -391,13 +400,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 tree.Arrange(new Rect(tree.DesiredSize));
 
                 var initial = new Scene(tree);
-                SceneBuilder.UpdateAll(initial);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(initial, new LayerDirtyRects());
 
                 border.IsVisible = false;
                 var result = initial.Clone();
 
-                Assert.True(SceneBuilder.Update(result, border));
-                Assert.False(SceneBuilder.Update(result, canvas));
+                Assert.True(sceneBuilder.Update(result, border, new LayerDirtyRects()));
+                Assert.False(sceneBuilder.Update(result, canvas, new LayerDirtyRects()));
 
                 var decoratorNode = (VisualNode)result.Root.Children[0];
                 Assert.Equal(0, decoratorNode.Children.Count);
@@ -433,7 +443,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 layout.ExecuteInitialLayoutPass(tree);
 
                 var scene = new Scene(tree);
-                SceneBuilder.UpdateAll(scene);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(scene, new LayerDirtyRects());
 
                 var borderNode = scene.FindNode(border);
                 var canvasNode = scene.FindNode(canvas);
@@ -444,7 +455,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 layout.ExecuteLayoutPass();
 
                 scene = scene.Clone();
-                SceneBuilder.Update(scene, decorator);
+                sceneBuilder.Update(scene, decorator, new LayerDirtyRects());
 
                 borderNode = scene.FindNode(border);
                 canvasNode = scene.FindNode(canvas);
@@ -480,7 +491,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 layout.ExecuteInitialLayoutPass(tree);
 
                 var scene = new Scene(tree);
-                SceneBuilder.UpdateAll(scene);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(scene, new LayerDirtyRects());
 
                 var borderNode = scene.FindNode(border);
                 var canvasNode = scene.FindNode(canvas);
@@ -492,14 +504,79 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
 
                 scene = scene.Clone();
 
-                var dirty = new DirtyRects();
-                SceneBuilder.Update(scene, decorator, dirty);
+                var dirty = new LayerDirtyRects();
+                sceneBuilder.Update(scene, decorator, dirty);
 
-                var rects = dirty.ToArray();
+                var rects = dirty.Single().Value.ToArray();
                 Assert.Equal(new[] { new Rect(0, 10, 100, 90) }, rects);
             }
         }
 
+        [Fact]
+        public void Control_With_Transparency_Should_Start_New_Layer()
+        {
+            using (TestApplication())
+            {
+                Decorator decorator;
+                Border border;
+                Canvas canvas;
+                var tree = new TestRoot
+                {
+                    Padding = new Thickness(10),
+                    Width = 100,
+                    Height = 120,
+                    Child = decorator = new Decorator
+                    {
+                        Padding = new Thickness(11),
+                        Child = border = new Border
+                        {
+                            Opacity = 0.5,
+                            Background = Brushes.Red,
+                            Padding = new Thickness(12),
+                            Child = canvas = new Canvas(),
+                        }
+                    }
+                };
+
+                var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
+                layout.ExecuteInitialLayoutPass(tree);
+
+                var dirty = new LayerDirtyRects();
+                var scene = new Scene(tree);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(scene, dirty);
+
+                var rootNode = (VisualNode)scene.Root;
+                var borderNode = (VisualNode)scene.FindNode(border);
+                var canvasNode = (VisualNode)scene.FindNode(canvas);
+
+                Assert.Same(tree, rootNode.LayerRoot);
+                Assert.Same(border, borderNode.LayerRoot);
+                Assert.Same(border, canvasNode.LayerRoot);
+
+                Assert.Equal(2, dirty.Count());
+                Assert.Empty(dirty.Select(x => x.Key).Except(new IVisual[] { tree, border }));
+
+                border.Opacity = 1;
+                scene = scene.Clone();
+
+                dirty = new LayerDirtyRects();
+                sceneBuilder.Update(scene, border, dirty);
+
+                rootNode = (VisualNode)scene.Root;
+                borderNode = (VisualNode)scene.FindNode(border);
+                canvasNode = (VisualNode)scene.FindNode(canvas);
+
+                Assert.Same(tree, rootNode.LayerRoot);
+                Assert.Same(tree, borderNode.LayerRoot);
+                Assert.Same(tree, canvasNode.LayerRoot);
+
+                Assert.Equal(1, dirty.Count());
+                Assert.Equal(tree, dirty.Single().Key);
+                Assert.Equal(new Rect(21, 21, 58, 78), dirty.Single().Value.Single());
+            }
+        }
+
         private IDisposable TestApplication()
         {
             return UnitTestApplication.Start(