Bläddra i källkod

Refactored DrawingContext and VisualBrush, added DrawingBrush (#10419)

Refactored DrawingContext and VisualBrush, added DrawingBrush
Nikita Tsukanov 2 år sedan
förälder
incheckning
ae1fcfed51
70 ändrade filer med 1094 tillägg och 945 borttagningar
  1. 10 10
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  2. 1 4
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  3. 1 3
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  4. 66 0
      src/Avalonia.Base/Media/DrawingBrush.cs
  5. 197 206
      src/Avalonia.Base/Media/DrawingContext.cs
  6. 42 97
      src/Avalonia.Base/Media/DrawingGroup.cs
  7. 1 1
      src/Avalonia.Base/Media/DrawingImage.cs
  8. 31 0
      src/Avalonia.Base/Media/ISceneBrush.cs
  9. 0 16
      src/Avalonia.Base/Media/IVisualBrush.cs
  10. 1 1
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  11. 12 6
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  12. 2 4
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  13. 0 66
      src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
  14. 112 0
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  15. 20 4
      src/Avalonia.Base/Media/VisualBrush.cs
  16. 1 5
      src/Avalonia.Base/Platform/IRenderTarget.cs
  17. 1 1
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  18. 31 6
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  19. 37 0
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
  20. 89 94
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  21. 2 28
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  22. 3 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  23. 0 29
      src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs
  24. 2 35
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  25. 6 9
      src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
  26. 1 3
      src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
  27. 24 15
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  28. 10 0
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  29. 9 28
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  30. 2 22
      src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  31. 1 3
      src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
  32. 5 25
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  33. 9 24
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  34. 9 1
      src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
  35. 3 18
      src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
  36. 3 16
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  37. 15 26
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  38. 13 39
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  39. 5 2
      src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs
  40. 1 1
      src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
  41. 1 1
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  42. 2 2
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  43. 1 1
      src/Avalonia.X11/X11CursorFactory.cs
  44. 1 1
      src/Avalonia.X11/X11IconLoader.cs
  45. 103 33
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  46. 1 2
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  47. 1 2
      src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs
  48. 1 2
      src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs
  49. 10 0
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  50. 55 0
      src/Skia/Avalonia.Skia/PictureRenderTarget.cs
  51. 1 2
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  52. 2 2
      src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs
  53. 4 4
      src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs
  54. 15 17
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  55. 1 1
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  56. 2 2
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  57. 4 4
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs
  58. 2 2
      src/Windows/Avalonia.Direct2D1/RenderTarget.cs
  59. 2 2
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  60. 1 1
      tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs
  61. 2 2
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  62. 2 2
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs
  63. 1 1
      tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs
  64. 2 2
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  65. 94 0
      tests/Avalonia.RenderTests/Media/TileBrushTests.cs
  66. 3 3
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  67. 2 2
      tests/Avalonia.UnitTests/TestRoot.cs
  68. BIN
      tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png
  69. BIN
      tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png
  70. BIN
      tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png

+ 10 - 10
samples/RenderDemo/Pages/CustomSkiaPage.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics;
 using System.Globalization;
+using System.Linq;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Media;
@@ -8,22 +9,27 @@ using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Skia;
 using Avalonia.Threading;
+using Avalonia.Utilities;
 using SkiaSharp;
 
 namespace RenderDemo.Pages
 {
     public class CustomSkiaPage : Control
     {
+        private readonly GlyphRun _noSkia;
         public CustomSkiaPage()
         {
             ClipToBounds = true;
+            var text = "Current rendering API is not Skia";
+            var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray();
+            _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs);
         }
         
         class CustomDrawOp : ICustomDrawOperation
         {
-            private readonly FormattedText _noSkia;
+            private readonly GlyphRun _noSkia;
 
-            public CustomDrawOp(Rect bounds, FormattedText noSkia)
+            public CustomDrawOp(Rect bounds, GlyphRun noSkia)
             {
                 _noSkia = noSkia;
                 Bounds = bounds;
@@ -42,10 +48,7 @@ namespace RenderDemo.Pages
             {
                 var leaseFeature = context.GetFeature<ISkiaSharpApiLeaseFeature>();
                 if (leaseFeature == null)
-                    using (var c = new DrawingContext(context, false))
-                    {
-                        c.DrawText(_noSkia, new Point());
-                    }
+                    context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl);
                 else
                 {
                     using var lease = leaseFeature.Lease();
@@ -114,10 +117,7 @@ namespace RenderDemo.Pages
         
         public override void Render(DrawingContext context)
         {
-            var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture,
-                FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
-
-            context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
+            context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), _noSkia));
             Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
         }
     }

+ 1 - 4
samples/RenderDemo/Pages/PathMeasurementPage.cs

@@ -37,11 +37,8 @@ namespace RenderDemo.Pages
 
         public override void Render(DrawingContext context)
         {
-            using (var ctxi = _bitmap.CreateDrawingContext(null))
-            using (var bitmapCtx = new DrawingContext(ctxi, false))
+            using (var bitmapCtx = _bitmap.CreateDrawingContext())
             {
-                ctxi.Clear(default);
-
                 var basePath = new PathGeometry();
 
                 using (var basePathCtx = basePath.Open())

+ 1 - 3
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@@ -28,13 +28,11 @@ namespace RenderDemo.Pages
         readonly Stopwatch _st = Stopwatch.StartNew();
         public override void Render(DrawingContext context)
         {
-            using (var ctxi = _bitmap.CreateDrawingContext(null))
-            using(var ctx = new DrawingContext(ctxi, false))
+            using (var ctx = _bitmap.CreateDrawingContext())
             using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
                                          * Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
                                          * Matrix.CreateTranslation(100, 100)))
             {
-                ctxi.Clear(default);
                 ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
             }
 

+ 66 - 0
src/Avalonia.Base/Media/DrawingBrush.cs

@@ -0,0 +1,66 @@
+using Avalonia.Media.Immutable;
+using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
+using Avalonia.Rendering.Composition.Drawing;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Paints an area with an <see cref="Drawing"/>.
+    /// </summary>
+    public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender
+    {
+        /// <summary>
+        /// Defines the <see cref="Drawing"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Drawing?> DrawingProperty =
+            AvaloniaProperty.Register<DrawingBrush, Drawing?>(nameof(Drawing));
+
+        static DrawingBrush()
+        {
+            AffectsRender<DrawingBrush>(DrawingProperty);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DrawingBrush"/> class.
+        /// </summary>
+        public DrawingBrush()
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DrawingBrush"/> class.
+        /// </summary>
+        /// <param name="visual">The visual to draw.</param>
+        public DrawingBrush(Drawing visual)
+        {
+            Drawing = visual;
+        }
+
+        /// <summary>
+        /// Gets or sets the visual to draw.
+        /// </summary>
+        public Drawing? Drawing
+        {
+            get { return GetValue(DrawingProperty); }
+            set { SetValue(DrawingProperty, value); }
+        }
+
+        ISceneBrushContent? ISceneBrush.CreateContent()
+        {
+            if (Drawing == null)
+                return null;
+            
+            
+            var recorder = new CompositionDrawingContext();
+            recorder.BeginUpdate(null);
+            Drawing?.Draw(recorder);
+            var drawList = recorder.EndUpdate();
+            if (drawList == null)
+                return null;
+            
+            return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList,
+                drawList.CalculateBounds(), true);
+        }
+    }
+}

+ 197 - 206
src/Avalonia.Base/Media/DrawingContext.cs

@@ -8,83 +8,45 @@ using Avalonia.Media.Imaging;
 
 namespace Avalonia.Media
 {
-    public sealed class DrawingContext : IDisposable
+    public abstract class DrawingContext : IDisposable
     {
-        private readonly bool _ownsImpl;
-        private int _currentLevel;
+        private static ThreadSafeObjectPool<Stack<RestoreState>> StateStackPool { get; } =
+            ThreadSafeObjectPool<Stack<RestoreState>>.Default;
 
+        private Stack<RestoreState>? _states;
 
-        private static ThreadSafeObjectPool<Stack<PushedState>> StateStackPool { get; } =
-            ThreadSafeObjectPool<Stack<PushedState>>.Default;
-
-        private static ThreadSafeObjectPool<Stack<TransformContainer>> TransformStackPool { get; } =
-            ThreadSafeObjectPool<Stack<TransformContainer>>.Default;
-
-        private Stack<PushedState>? _states = StateStackPool.Get();
-
-        private Stack<TransformContainer>? _transformContainers = TransformStackPool.Get();
-
-        readonly struct TransformContainer
-        {
-            public readonly Matrix LocalTransform;
-            public readonly Matrix ContainerTransform;
-
-            public TransformContainer(Matrix localTransform, Matrix containerTransform)
-            {
-                LocalTransform = localTransform;
-                ContainerTransform = containerTransform;
-            }
-        }
-
-        public DrawingContext(IDrawingContextImpl impl)
+        internal DrawingContext()
         {
-            PlatformImpl = impl;
-            _ownsImpl = true;
+            
         }
-        
-        public DrawingContext(IDrawingContextImpl impl, bool ownsImpl)
-        {
-            _ownsImpl = ownsImpl;
-            PlatformImpl = impl;
-        }
-
-        public IDrawingContextImpl PlatformImpl { get; }
-
-        private Matrix _currentTransform = Matrix.Identity;
 
-        private Matrix _currentContainerTransform = Matrix.Identity;
-
-        /// <summary>
-        /// Gets the current transform of the drawing context.
-        /// </summary>
-        public Matrix CurrentTransform
+        public void Dispose()
         {
-            get { return _currentTransform; }
-            private set
+            if (_states != null)
             {
-                _currentTransform = value;
-                var transform = _currentTransform * _currentContainerTransform;
-                PlatformImpl.Transform = transform;
-            }
-        }
+                while (_states.Count > 0)
+                    _states.Pop().Dispose();
 
-        //HACK: This is a temporary hack that is used in the render loop 
-        //to update TransformedBounds property
-        [Obsolete("HACK for render loop, don't use")]
-        public Matrix CurrentContainerTransform => _currentContainerTransform;
+                StateStackPool.ReturnAndSetNull(ref _states);
+            }
 
+            DisposeCore();
+        }
+        
+        protected abstract void DisposeCore();
+        
         /// <summary>
         /// Draws an image.
         /// </summary>
         /// <param name="source">The image.</param>
         /// <param name="rect">The rect in the output to draw to.</param>
-        public void DrawImage(IImage source, Rect rect)
+        public virtual void DrawImage(IImage source, Rect rect)
         {
             _ = source ?? throw new ArgumentNullException(nameof(source));
-
             DrawImage(source, new Rect(source.Size), rect);
         }
 
+
         /// <summary>
         /// Draws an image.
         /// </summary>
@@ -92,12 +54,22 @@ namespace Avalonia.Media
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
-        public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
+        public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect,
+            BitmapInterpolationMode bitmapInterpolationMode = default)
         {
             _ = source ?? throw new ArgumentNullException(nameof(source));
-
             source.Draw(this, sourceRect, destRect, bitmapInterpolationMode);
         }
+        
+        /// <summary>
+        /// Draws a platform-specific bitmap impl.
+        /// </summary>
+        /// <param name="source">The bitmap image.</param>
+        /// <param name="opacity">The opacity to draw with.</param>
+        /// <param name="sourceRect">The rect in the image to draw.</param>
+        /// <param name="destRect">The rect in the output to draw to.</param>
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
+        internal abstract void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
 
         /// <summary>
         /// Draws a line.
@@ -108,11 +80,11 @@ namespace Avalonia.Media
         public void DrawLine(IPen pen, Point p1, Point p2)
         {
             if (PenIsVisible(pen))
-            {
-                PlatformImpl.DrawLine(pen, p1, p2);
-            }
+                DrawLineCore(pen, p1, p2);
         }
 
+        protected abstract void DrawLineCore(IPen pen, Point p1, Point p2);
+
         /// <summary>
         /// Draws a geometry.
         /// </summary>
@@ -121,10 +93,10 @@ namespace Avalonia.Media
         /// <param name="geometry">The geometry.</param>
         public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry)
         {
-            if (geometry.PlatformImpl is not null)
-                DrawGeometry(brush, pen, geometry.PlatformImpl);
+            if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null)
+                DrawGeometryCore(brush, pen, geometry.PlatformImpl);
         }
-
+        
         /// <summary>
         /// Draws a geometry.
         /// </summary>
@@ -133,14 +105,12 @@ namespace Avalonia.Media
         /// <param name="geometry">The geometry.</param>
         public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
         {
-            _ = geometry ?? throw new ArgumentNullException(nameof(geometry));
-
-            if (brush != null || PenIsVisible(pen))
-            {
-                PlatformImpl.DrawGeometry(brush, pen, geometry);
-            }
+            if ((brush != null || PenIsVisible(pen)))
+                DrawGeometryCore(brush, pen, geometry);
         }
 
+        protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry);
+
         /// <summary>
         /// Draws a rectangle with the specified Brush and Pen.
         /// </summary>
@@ -158,14 +128,12 @@ namespace Avalonia.Media
         /// The brush and the pen can both be null. If the brush is null, then no fill is performed.
         /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
         /// </remarks>
-        public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0,
+        public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect,
+            double radiusX = 0, double radiusY = 0,
             BoxShadows boxShadows = default)
         {
             if (brush == null && !PenIsVisible(pen))
-            {
                 return;
-            }
-
             if (!MathUtilities.IsZero(radiusX))
             {
                 radiusX = Math.Min(radiusX, rect.Width / 2);
@@ -175,20 +143,48 @@ namespace Avalonia.Media
             {
                 radiusY = Math.Min(radiusY, rect.Height / 2);
             }
-
-            PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
+            
+            DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
+        }
+        
+        /// <summary>
+        /// Draws a rectangle with the specified Brush and Pen.
+        /// </summary>
+        /// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
+        /// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
+        /// <param name="rrect">The rectangle bounds.</param>
+        /// <param name="boxShadows">Box shadow effect parameters</param>
+        /// <remarks>
+        /// The brush and the pen can both be null. If the brush is null, then no fill is performed.
+        /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
+        /// </remarks>
+        public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
+        {
+            if (brush == null && !PenIsVisible(pen))
+                return;
+            DrawRectangleCore(brush, pen, rrect, boxShadows);
         }
 
+        protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect,
+            BoxShadows boxShadows = default);
+        
         /// <summary>
         /// Draws the outline of a rectangle.
         /// </summary>
         /// <param name="pen">The pen.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
-        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f)
-        {
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) =>
             DrawRectangle(null, pen, rect, cornerRadius, cornerRadius);
-        }
+
+        /// <summary>
+        /// Draws a filled rectangle.
+        /// </summary>
+        /// <param name="brush">The brush.</param>
+        /// <param name="rect">The rectangle bounds.</param>
+        /// <param name="cornerRadius">The corner radius.</param>
+        public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) => 
+            DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
 
         /// <summary>
         /// Draws an ellipse with the specified Brush and Pen.
@@ -204,35 +200,50 @@ namespace Avalonia.Media
         /// </remarks>
         public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY)
         {
-            if (brush == null && !PenIsVisible(pen))
+            if (brush != null || PenIsVisible(pen))
             {
-                return;
+                var originX = center.X - radiusX;
+                var originY = center.Y - radiusY;
+                var width = radiusX * 2;
+                var height = radiusY * 2;
+                DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height));
             }
-
-            var originX = center.X - radiusX;
-            var originY = center.Y - radiusY;
-            var width = radiusX * 2;
-            var height = radiusY * 2;
-
-            PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height));
+        }        
+        
+        /// <summary>
+        /// Draws an ellipse with the specified Brush and Pen.
+        /// </summary>
+        /// <param name="brush">The brush used to fill the ellipse, or <c>null</c> for no fill.</param>
+        /// <param name="pen">The pen used to stroke the ellipse, or <c>null</c> for no stroke.</param>
+        /// <param name="rect">The bounding rect.</param>
+        /// <remarks>
+        /// The brush and the pen can both be null. If the brush is null, then no fill is performed.
+        /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
+        /// </remarks>
+        public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
+        {
+            if (brush != null || PenIsVisible(pen))
+                DrawEllipseCore(brush, pen, rect);
         }
 
+        protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect);
+
         /// <summary>
         /// Draws a custom drawing operation
         /// </summary>
         /// <param name="custom">custom operation</param>
-        public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom);
+        public abstract void Custom(ICustomDrawOperation custom);
 
         /// <summary>
         /// Draws text.
         /// </summary>
         /// <param name="origin">The upper-left corner of the text.</param>
         /// <param name="text">The text.</param>
-        public void DrawText(FormattedText text, Point origin)
+        public virtual void DrawText(FormattedText text, Point origin)
         {
             _ = text ?? throw new ArgumentNullException(nameof(text));
          
-           text.Draw(this, origin);            
+            text.Draw(this, origin);            
         }
 
         /// <summary>
@@ -240,30 +251,31 @@ namespace Avalonia.Media
         /// </summary>
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="glyphRun">The glyph run.</param>
-        public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
+        public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun);
+
+        public record struct PushedState : IDisposable
         {
-            _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
+            private readonly DrawingContext _context;
+            private readonly int _level;
 
-            if (foreground != null)
+            public PushedState(DrawingContext context)
             {
-                PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl);
+                _context = context;
+                _level = _context._states!.Count;
             }
-        }
 
-        /// <summary>
-        /// Draws a filled rectangle.
-        /// </summary>
-        /// <param name="brush">The brush.</param>
-        /// <param name="rect">The rectangle bounds.</param>
-        /// <param name="cornerRadius">The corner radius.</param>
-        public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f)
-        {
-            DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
+            public void Dispose()
+            {
+                if(_context?._states == null)
+                    return;
+                if(_context._states.Count != _level)
+                    throw new InvalidOperationException("Wrong Push/Pop state order");
+                _context._states.Pop().Dispose();
+            }
         }
-
-        public readonly record struct PushedState : IDisposable
+        
+        private readonly record struct RestoreState : IDisposable
         {
-            private readonly int _level;
             private readonly DrawingContext _context;
             private readonly Matrix _matrix;
             private readonly PushedStateType _type;
@@ -271,62 +283,56 @@ namespace Avalonia.Media
             public enum PushedStateType
             {
                 None,
-                Matrix,
+                Transform,
                 Opacity,
                 Clip,
-                MatrixContainer,
                 GeometryClip,
                 OpacityMask,
+                BitmapBlendMode
             }
 
-            public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default)
+            public RestoreState(DrawingContext context, PushedStateType type)
             {
-                if (context._states is null)
-                    throw new ObjectDisposedException(nameof(DrawingContext));
-
                 _context = context;
                 _type = type;
-                _matrix = matrix;
-                _level = context._currentLevel += 1;
-                context._states.Push(this);
             }
 
             public void Dispose()
             {
                 if (_type == PushedStateType.None)
                     return;
-                if (_context._states is null || _context._transformContainers is null)
+                if (_context._states is null)
                     throw new ObjectDisposedException(nameof(DrawingContext));
-                if (_context._currentLevel != _level)
-                    throw new InvalidOperationException("Wrong Push/Pop state order");
-                _context._currentLevel--;
-                _context._states.Pop();
-                if (_type == PushedStateType.Matrix)
-                    _context.CurrentTransform = _matrix;
+                if (_type == PushedStateType.Transform)
+                    _context.PopTransformCore();
                 else if (_type == PushedStateType.Clip)
-                    _context.PlatformImpl.PopClip();
+                    _context.PopClipCore();
                 else if (_type == PushedStateType.Opacity)
-                    _context.PlatformImpl.PopOpacity();
+                    _context.PopOpacityCore();
                 else if (_type == PushedStateType.GeometryClip)
-                    _context.PlatformImpl.PopGeometryClip();
+                    _context.PopGeometryClipCore();
                 else if (_type == PushedStateType.OpacityMask)
-                    _context.PlatformImpl.PopOpacityMask();
-                else if (_type == PushedStateType.MatrixContainer)
-                {
-                    var cont = _context._transformContainers.Pop();
-                    _context._currentContainerTransform = cont.ContainerTransform;
-                    _context.CurrentTransform = cont.LocalTransform;
-                }
+                    _context.PopOpacityMaskCore();
+                else if (_type == PushedStateType.BitmapBlendMode)
+                    _context.PopBitmapBlendModeCore();
             }
         }
 
-
+        /// <summary>
+        /// Pushes a clip rectangle.
+        /// </summary>
+        /// <param name="clip">The clip rectangle.</param>
+        /// <returns>A disposable used to undo the clip rectangle.</returns>
         public PushedState PushClip(RoundedRect clip)
         {
-            PlatformImpl.PushClip(clip);
-            return new PushedState(this, PushedState.PushedStateType.Clip);
+            PushClipCore(clip);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip));
+            return new PushedState(this);
         }
 
+        protected abstract void PushClipCore(RoundedRect rect);
+
         /// <summary>
         /// Pushes a clip rectangle.
         /// </summary>
@@ -334,9 +340,13 @@ namespace Avalonia.Media
         /// <returns>A disposable used to undo the clip rectangle.</returns>
         public PushedState PushClip(Rect clip)
         {
-            PlatformImpl.PushClip(clip);
-            return new PushedState(this, PushedState.PushedStateType.Clip);
+            PushClipCore(clip);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip));
+            return new PushedState(this);
         }
+        
+        protected abstract void PushClipCore(Rect rect);
 
         /// <summary>
         /// Pushes a clip geometry.
@@ -345,17 +355,13 @@ namespace Avalonia.Media
         /// <returns>A disposable used to undo the clip geometry.</returns>
         public PushedState PushGeometryClip(Geometry clip)
         {
-            _ = clip ?? throw new ArgumentNullException(nameof(clip));
-
-            // HACK: This check was added when nullable annotations pointed out that we're potentially
-            // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but
-            // I don't want to make that change as part of adding nullable annotations.
-            if (clip.PlatformImpl is null)
-                throw new InvalidOperationException("Cannot push empty geometry clip.");
-
-            PlatformImpl.PushGeometryClip(clip.PlatformImpl);
-            return new PushedState(this, PushedState.PushedStateType.GeometryClip);
+            PushGeometryClipCore(clip);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip));
+            return new PushedState(this);
         }
+        
+        protected abstract void PushGeometryClipCore(Geometry clip);
 
         /// <summary>
         /// Pushes an opacity value.
@@ -364,11 +370,13 @@ namespace Avalonia.Media
         /// <param name="bounds">The bounds.</param>
         /// <returns>A disposable used to undo the opacity.</returns>
         public PushedState PushOpacity(double opacity, Rect bounds)
-        //TODO: Eliminate platform-specific push opacity call
         {
-            PlatformImpl.PushOpacity(opacity, bounds);
-            return new PushedState(this, PushedState.PushedStateType.Opacity);
+            PushOpacityCore(opacity, bounds);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity));
+            return new PushedState(this);
         }
+        protected abstract void PushOpacityCore(double opacity, Rect bounds);
 
         /// <summary>
         /// Pushes an opacity mask.
@@ -380,70 +388,53 @@ namespace Avalonia.Media
         /// <returns>A disposable to undo the opacity mask.</returns>
         public PushedState PushOpacityMask(IBrush mask, Rect bounds)
         {
-            PlatformImpl.PushOpacityMask(mask, bounds);
-            return new PushedState(this, PushedState.PushedStateType.OpacityMask);
+            PushOpacityMaskCore(mask, bounds);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask));
+            return new PushedState(this);
         }
+        protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds);
 
-        /// <summary>
-        /// Pushes a matrix post-transformation.
-        /// </summary>
-        /// <param name="matrix">The matrix</param>
-        /// <returns>A disposable used to undo the transformation.</returns>
-        public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix);
-
-        /// <summary>
-        /// Pushes a matrix pre-transformation.
-        /// </summary>
-        /// <param name="matrix">The matrix</param>
-        /// <returns>A disposable used to undo the transformation.</returns>
-        public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform);
-
-        /// <summary>
-        /// Sets the current matrix transformation.
-        /// </summary>
-        /// <param name="matrix">The matrix</param>
-        /// <returns>A disposable used to undo the transformation.</returns>
-        public PushedState PushSetTransform(Matrix matrix)
+        public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode)
         {
-            var oldMatrix = CurrentTransform;
-            CurrentTransform = matrix;
-
-            return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix);
+            PushBitmapBlendMode(blendingMode);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode));
+            return new PushedState(this);
         }
 
-        /// <summary>
-        /// Pushes a new transform context.
-        /// </summary>
-        /// <returns>A disposable used to undo the transformation.</returns>
-        public PushedState PushTransformContainer()
-        {
-            if (_transformContainers is null)
-                throw new ObjectDisposedException(nameof(DrawingContext));
-            _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform));
-            _currentContainerTransform = CurrentTransform * _currentContainerTransform;
-            _currentTransform = Matrix.Identity;
-            return new PushedState(this, PushedState.PushedStateType.MatrixContainer);
-        }
+        protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode);
 
         /// <summary>
-        /// Disposes of any resources held by the <see cref="DrawingContext"/>.
+        /// Pushes a matrix transformation.
         /// </summary>
-        public void Dispose()
+        /// <param name="matrix">The matrix</param>
+        /// <returns>A disposable used to undo the transformation.</returns>
+        public PushedState PushTransform(Matrix matrix)
         {
-            if (_states is null || _transformContainers is null)
-                throw new ObjectDisposedException(nameof(DrawingContext));
-            while (_states.Count != 0)
-                _states.Peek().Dispose();
-            StateStackPool.Return(_states);
-            _states = null;
-            if (_transformContainers.Count != 0)
-                throw new InvalidOperationException("Transform container stack is non-empty");
-            TransformStackPool.Return(_transformContainers);
-            _transformContainers = null;
-            if (_ownsImpl)
-                PlatformImpl.Dispose();
+            PushTransformCore(matrix);
+            _states ??= StateStackPool.Get();
+            _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform));
+            return new PushedState(this);
         }
 
+        [Obsolete("Use PushTransform")]
+        public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix);
+        [Obsolete("Use PushTransform")]
+        public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix);
+        [Obsolete("Use PushTransform")]
+        public PushedState PushTransformContainer() => PushTransform(Matrix.Identity);
+        
+        
+        protected abstract void PushTransformCore(Matrix matrix);
+
+        protected abstract void PopClipCore();
+        protected abstract void PopGeometryClipCore();
+        protected abstract void PopOpacityCore();
+        protected abstract void PopOpacityMaskCore();
+        protected abstract void PopBitmapBlendModeCore();
+        protected abstract void PopTransformCore();
+        
         private static bool PenIsVisible(IPen? pen)
         {
             return pen?.Brush != null && pen.Thickness > 0;

+ 42 - 97
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -67,10 +67,7 @@ namespace Avalonia.Media
             }
         }
 
-        public DrawingContext Open()
-        {
-            return new DrawingContext(new DrawingGroupDrawingContext(this));
-        }
+        public DrawingContext Open() => new DrawingGroupDrawingContext(this);
 
         public override void Draw(DrawingContext context)
         {
@@ -105,7 +102,7 @@ namespace Avalonia.Media
             return rect;
         }
 
-        private class DrawingGroupDrawingContext : IDrawingContextImpl
+        private sealed class DrawingGroupDrawingContext : DrawingContext
         {
             private readonly DrawingGroup _drawingGroup;
             private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
@@ -135,17 +132,7 @@ namespace Avalonia.Media
                 _drawingGroup = drawingGroup;
             }
 
-            public Matrix Transform
-            {
-                get => _transform;
-                set
-                {
-                    _transform = value;
-                    PushTransform(new MatrixTransform(value));
-                }
-            }
-
-            public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
+            protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
             {
                 if ((brush == null) && (pen == null))
                 {
@@ -159,7 +146,7 @@ namespace Avalonia.Media
                 AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
             }
 
-            public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
+            protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
             {
                 if ((brush == null) && (pen == null))
                 {
@@ -169,7 +156,7 @@ namespace Avalonia.Media
                 AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
             }
 
-            public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
+            public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
             {
                 if (foreground == null)
                 {
@@ -179,124 +166,70 @@ namespace Avalonia.Media
                 GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
                 {
                     Foreground = foreground,
-                    GlyphRun = new GlyphRun(glyphRun)
+                    GlyphRun = glyphRun
                 };
 
                 // Add Drawing to the Drawing graph
                 AddDrawing(glyphRunDrawing);
             }
 
-            public void DrawLine(IPen? pen, Point p1, Point p2)
-            {
-                if (pen == null)
-                {
-                    return;
-                }
-
-                // Instantiate the geometry
-                var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
-
-                // Add Drawing to the Drawing graph
-                AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
-            }
-
-            public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
-            {
-                if ((brush == null) && (pen == null))
-                {
-                    return;
-                }
-
-                // Instantiate the geometry
-                var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect);
-
-                // Add Drawing to the Drawing graph
-                AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
-            }
-
-            public void Clear(Color color)
+            protected override void PushClipCore(RoundedRect rect)
             {
                 throw new NotImplementedException();
             }
 
-            public IDrawingContextLayerImpl CreateLayer(Size size)
+            protected override void PushClipCore(Rect rect)
             {
                 throw new NotImplementedException();
             }
 
-            public void Custom(ICustomDrawOperation custom)
+            protected override void PushGeometryClipCore(Geometry clip)
             {
                 throw new NotImplementedException();
             }
 
-            public object? GetFeature(Type t) => null;
-
-            public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
+            protected override void PushOpacityCore(double opacity, Rect bounds)
             {
                 throw new NotImplementedException();
             }
 
-            public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+            protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
             {
                 throw new NotImplementedException();
             }
 
-            public void PopBitmapBlendMode()
+            protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
             {
                 throw new NotImplementedException();
             }
 
-            public void PopClip()
+            internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
+                BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
             {
                 throw new NotImplementedException();
             }
 
-            public void PopGeometryClip()
+            protected override void DrawLineCore(IPen pen, Point p1, Point p2)
             {
-                throw new NotImplementedException();
-            }
-
-            public void PopOpacity()
-            {
-                throw new NotImplementedException();
-            }
-
-            public void PopOpacityMask()
-            {
-                throw new NotImplementedException();
-            }
-
-            public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void PushClip(Rect clip)
-            {
-                throw new NotImplementedException();
-            }
+                // Instantiate the geometry
+                var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
 
-            public void PushClip(RoundedRect clip)
-            {
-                throw new NotImplementedException();
+                // Add Drawing to the Drawing graph
+                AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
             }
 
-            public void PushGeometryClip(IGeometryImpl clip)
+            protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
             {
-                throw new NotImplementedException();
-            }
+                // Instantiate the geometry
+                var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect);
 
-            public void PushOpacity(double opacity, Rect bounds)
-            {
-                throw new NotImplementedException();
+                // Add Drawing to the Drawing graph
+                AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
             }
 
-            public void PushOpacityMask(IBrush mask, Rect bounds)
-            {
-                throw new NotImplementedException();
-            }
+            public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException();
 
-            public void Dispose()
+            protected override void DisposeCore()
             {
                 // Dispose may be called multiple times without throwing
                 // an exception.
@@ -366,22 +299,34 @@ namespace Avalonia.Media
                 // Restore the previous value of the current drawing group
                 _currentDrawingGroup = _previousDrawingGroupStack.Pop();
             }
-
+            
             /// <summary>
             ///     PushTransform -
             ///     Push a Transform which will apply to all drawing operations until the corresponding
             ///     Pop.
             /// </summary>
-            /// <param name="transform"> The Transform to push. </param>
-            private void PushTransform(Transform transform)
+            /// <param name="matrix"> The transform to push. </param>
+            protected override void PushTransformCore(Matrix matrix)
             {
                 // Instantiate a new drawing group and set it as the _currentDrawingGroup
                 var drawingGroup = PushNewDrawingGroup();
 
                 // Set the transform on the new DrawingGroup
-                drawingGroup.Transform = transform;
+                drawingGroup.Transform = new MatrixTransform(matrix);
             }
 
+            protected override void PopClipCore() => Pop();
+
+            protected override void PopGeometryClipCore() => Pop();
+
+            protected override void PopOpacityCore() => Pop();
+
+            protected override void PopOpacityMaskCore() => Pop();
+
+            protected override void PopBitmapBlendModeCore() => Pop();
+
+            protected override void PopTransformCore() => Pop();
+
             /// <summary>
             /// Creates a new DrawingGroup for a Push* call by setting the
             /// _currentDrawingGroup to a newly instantiated DrawingGroup,

+ 1 - 1
src/Avalonia.Base/Media/DrawingImage.cs

@@ -62,7 +62,7 @@ namespace Avalonia.Media
                 -sourceRect.Y + destRect.Y - bounds.Y);
 
             using (context.PushClip(destRect))
-            using (context.PushPreTransform(translate * scale))
+            using (context.PushTransform(translate * scale))
             {
                 Drawing?.Draw(context);
             }

+ 31 - 0
src/Avalonia.Base/Media/ISceneBrush.cs

@@ -0,0 +1,31 @@
+using System;
+using Avalonia.Media.Imaging;
+using Avalonia.Media.Immutable;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition.Drawing;
+
+namespace Avalonia.Media
+{
+    [NotClientImplementable]
+    public interface ISceneBrush : ITileBrush
+    {
+        ISceneBrushContent? CreateContent();
+    }
+    
+    [NotClientImplementable]
+    public interface ISceneBrushContent : IImmutableBrush, IDisposable
+    {
+        ITileBrush Brush { get; }
+        Rect Rect { get; }
+        void Render(IDrawingContextImpl context, Matrix? transform);
+        internal bool UseScalableRasterization { get; }
+    }
+
+    internal class ImmutableSceneBrush : ImmutableTileBrush
+    {
+        public ImmutableSceneBrush(ITileBrush source) : base(source)
+        {
+        }
+    }
+}

+ 0 - 16
src/Avalonia.Base/Media/IVisualBrush.cs

@@ -1,16 +0,0 @@
-using Avalonia.Metadata;
-
-namespace Avalonia.Media
-{
-    /// <summary>
-    /// Paints an area with an <see cref="Visual"/>.
-    /// </summary>
-    [NotClientImplementable]
-    public interface IVisualBrush : ITileBrush
-    {
-        /// <summary>
-        /// Gets the visual to draw.
-        /// </summary>
-        Visual? Visual { get; }
-    }
-}

+ 1 - 1
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging
             Rect destRect,
             BitmapInterpolationMode bitmapInterpolationMode)
         {
-            context.PlatformImpl.DrawBitmap(
+            context.DrawBitmap(
                 PlatformImpl,
                 1,
                 sourceRect,

+ 12 - 6
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging
     /// <summary>
     /// A bitmap that holds the rendering of a <see cref="Visual"/>.
     /// </summary>
-    public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget
+    public class RenderTargetBitmap : Bitmap, IDisposable
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
@@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging
         /// Renders a visual to the <see cref="RenderTargetBitmap"/>.
         /// </summary>
         /// <param name="visual">The visual to render.</param>
-        public void Render(Visual visual) => ImmediateRenderer.Render(visual, this);
+        public void Render(Visual visual)
+        {
+            using (var ctx = CreateDrawingContext())
+                ImmediateRenderer.Render(visual, ctx);
+        }
 
         /// <summary>
         /// Creates a platform-specific implementation for a <see cref="RenderTargetBitmap"/>.
@@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging
             return factory.CreateRenderTargetBitmap(size, dpi);
         }
 
-        /// <inheritdoc/>
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr);
-
-        bool IRenderTarget.IsCorrupted => false;
+        public DrawingContext CreateDrawingContext()
+        {
+            var platform = PlatformImpl.Item.CreateDrawingContext();
+            platform.Clear(Colors.Transparent);
+            return new PlatformDrawingContext(platform);
+        }
     }
 }

+ 2 - 4
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@@ -354,12 +354,10 @@ namespace Avalonia.Media
                 throw new ObjectDisposedException(nameof(DrawingContext));
             while (_states.Count != 0)
                 _states.Peek().Dispose();
-            StateStackPool.Return(_states);
-            _states = null;
+            StateStackPool.ReturnAndSetNull(ref _states);
             if (_transformContainers.Count != 0)
                 throw new InvalidOperationException("Transform container stack is non-empty");
-            TransformStackPool.Return(_transformContainers);
-            _transformContainers = null;
+            TransformStackPool.ReturnAndSetNull(ref _transformContainers);
             if (_ownsImpl)
                 PlatformImpl.Dispose();
         }

+ 0 - 66
src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs

@@ -1,66 +0,0 @@
-using Avalonia.Media.Imaging;
-
-namespace Avalonia.Media.Immutable
-{
-    /// <summary>
-    /// Paints an area with an <see cref="Visual"/>.
-    /// </summary>
-    internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ImmutableImageBrush"/> class.
-        /// </summary>
-        /// <param name="visual">The visual to draw.</param>
-        /// <param name="alignmentX">The horizontal alignment of a tile in the destination.</param>
-        /// <param name="alignmentY">The vertical alignment of a tile in the destination.</param>
-        /// <param name="destinationRect">The rectangle on the destination in which to paint a tile.</param>
-        /// <param name="opacity">The opacity of the brush.</param>
-        /// <param name="transform">The transform of the brush.</param>
-        /// <param name="transformOrigin">The transform origin of the brush</param>
-        /// <param name="sourceRect">The rectangle of the source image that will be displayed.</param>
-        /// <param name="stretch">
-        /// How the source rectangle will be stretched to fill the destination rect.
-        /// </param>
-        /// <param name="tileMode">The tile mode.</param>
-        /// <param name="bitmapInterpolationMode">Controls the quality of interpolation.</param>
-        public ImmutableVisualBrush(
-            Visual? visual,
-            AlignmentX alignmentX = AlignmentX.Center,
-            AlignmentY alignmentY = AlignmentY.Center,
-            RelativeRect? destinationRect = null,
-            double opacity = 1,
-            ImmutableTransform? transform = null,
-            RelativePoint transformOrigin = default,
-            RelativeRect? sourceRect = null,
-            Stretch stretch = Stretch.Uniform,
-            TileMode tileMode = TileMode.None,
-            BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
-            : base(
-                  alignmentX,
-                  alignmentY,
-                  destinationRect ?? RelativeRect.Fill,
-                  opacity,
-                  transform,
-                  transformOrigin,
-                  sourceRect ?? RelativeRect.Fill,
-                  stretch,
-                  tileMode,
-                  bitmapInterpolationMode)
-        {
-            Visual = visual;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ImmutableVisualBrush"/> class.
-        /// </summary>
-        /// <param name="source">The brush from which this brush's properties should be copied.</param>
-        public ImmutableVisualBrush(IVisualBrush source)
-            : base(source)
-        {
-            Visual = source.Visual;
-        }
-
-        /// <inheritdoc/>
-        public Visual? Visual { get; }
-    }
-}

+ 112 - 0
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media.Imaging;
+using Avalonia.Media.Immutable;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media;
+
+internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
+{
+    private readonly IDrawingContextImpl _impl;
+    private readonly bool _ownsImpl;
+    private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
+        ThreadSafeObjectPool<Stack<Matrix>>.Default;
+
+    private Stack<Matrix>? _transforms;
+        
+
+    public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true)
+    {
+        _impl = impl;
+        _ownsImpl = ownsImpl;
+    }
+
+    protected override void DrawLineCore(IPen pen, Point p1, Point p2) =>
+        _impl.DrawLine(pen, p1, p2);
+
+    protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) =>
+        _impl.DrawGeometry(brush, pen, geometry);
+
+    protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect,
+        BoxShadows boxShadows = default) =>
+        _impl.DrawRectangle(brush, pen, rrect, boxShadows);
+
+    protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect);
+
+    internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
+        BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) =>
+        _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
+
+    public override void Custom(ICustomDrawOperation custom) =>
+        custom.Render(_impl);
+
+    public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
+    {
+        _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
+
+        if (foreground != null) 
+            _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl);
+    }
+
+    protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect);
+
+    protected override void PushClipCore(Rect rect) => _impl.PushClip(rect);
+
+    protected override void PushGeometryClipCore(Geometry clip) =>
+        _impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException());
+
+    protected override void PushOpacityCore(double opacity, Rect bounds) => 
+        _impl.PushOpacity(opacity, bounds);
+
+    protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) =>
+        _impl.PushOpacityMask(mask, bounds);
+
+    protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) =>
+        _impl.PushBitmapBlendMode(blendingMode);
+
+    protected override void PushTransformCore(Matrix matrix)
+    {
+        _transforms ??= TransformStackPool.Get();
+        var current = _impl.Transform;
+        _transforms.Push(current);
+        _impl.Transform = matrix * current;
+    }
+
+    protected override void PopClipCore() => _impl.PopClip();
+
+    protected override void PopGeometryClipCore() => _impl.PopGeometryClip();
+
+    protected override void PopOpacityCore() => _impl.PopOpacity();
+
+    protected override void PopOpacityMaskCore() => _impl.PopOpacityMask();
+
+    protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode();
+
+    protected override void PopTransformCore() =>
+        _impl.Transform =
+            (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop();
+
+    protected override void DisposeCore()
+    {
+        if (_ownsImpl)
+            _impl.Dispose();
+        if (_transforms != null)
+        {
+            if (_transforms.Count != 0)
+                throw new InvalidOperationException("Not all states are disposed");
+            TransformStackPool.ReturnAndSetNull(ref _transforms);
+        }
+    }
+
+    public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
+    {
+        if (_impl is IDrawingContextWithAcrylicLikeSupport idc)
+            idc.DrawRectangle(material, rect);
+        else
+            DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect);
+    }
+}

+ 20 - 4
src/Avalonia.Base/Media/VisualBrush.cs

@@ -1,11 +1,14 @@
 using Avalonia.Media.Immutable;
+using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
+using Avalonia.Rendering.Composition.Drawing;
 
 namespace Avalonia.Media
 {
     /// <summary>
     /// Paints an area with an <see cref="Visual"/>.
     /// </summary>
-    public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush
+    public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender
     {
         /// <summary>
         /// Defines the <see cref="Visual"/> property.
@@ -43,10 +46,23 @@ namespace Avalonia.Media
             set { SetValue(VisualProperty, value); }
         }
 
-        /// <inheritdoc/>
-        IImmutableBrush IMutableBrush.ToImmutable()
+        ISceneBrushContent? ISceneBrush.CreateContent()
         {
-            return new ImmutableVisualBrush(this);
+            if (Visual == null)
+                return null;
+
+            if (Visual is IVisualBrushInitialize initialize)
+                initialize.EnsureInitialized();
+            
+            var recorder = new CompositionDrawingContext();
+            recorder.BeginUpdate(null);
+            ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
+            var drawList = recorder.EndUpdate();
+            if (drawList == null)
+                return null;
+
+            return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList,
+                new(Visual.Bounds.Size), false);
         }
     }
 }

+ 1 - 5
src/Avalonia.Base/Platform/IRenderTarget.cs

@@ -14,11 +14,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
         /// </summary>
-        /// <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);
+        IDrawingContextImpl CreateDrawingContext();
         
         /// <summary>
         /// Indicates if the render target is no longer usable and needs to be recreated

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor
     {
         _root = root;
         _compositor = compositor;
-        _recordingContext = new DrawingContext(_recorder);
+        _recordingContext = _recorder;
         CompositionTarget = compositor.CreateCompositionTarget(surfaces);
         CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
         _update = Update;

+ 31 - 6
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Collections.Pooled;
+using Avalonia.Platform;
 using Avalonia.Rendering.Composition.Server;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Utilities;
@@ -13,8 +14,6 @@ namespace Avalonia.Rendering.Composition.Drawing;
 /// </summary>
 internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
 {
-    public Size? Size { get; set; }
-    
     public CompositionDrawList()
     {
         
@@ -34,21 +33,47 @@ internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
 
     public CompositionDrawList Clone()
     {
-        var clone = new CompositionDrawList(Count) { Size = Size };
+        var clone = new CompositionDrawList(Count);
         foreach (var r in this)
             clone.Add(r.Clone());
         return clone;
     }
 
-    public void Render(CompositorDrawingContextProxy canvas)
+    public void Render(IDrawingContextImpl canvas)
+    {
+        foreach (var cmd in this)
+        {
+            if (cmd.Item is IDrawOperationWithTransform hasTransform)
+                canvas.Transform = hasTransform.Transform;
+            cmd.Item.Render(canvas);
+        }
+    }
+    
+    public void Render(IDrawingContextImpl canvas, Matrix transform)
     {
         foreach (var cmd in this)
         {
-            canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList;
+            if (cmd.Item is IDrawOperationWithTransform hasTransform)
+                canvas.Transform = hasTransform.Transform * transform;
             cmd.Item.Render(canvas);
         }
+    }
+
 
-        canvas.VisualBrushDrawList = null;
+    public Rect CalculateBounds()
+    {
+        var rect = default(Rect);
+        foreach (var cmd in this)
+            rect = rect.Union(cmd.Item.Bounds);
+        return rect;
+    }
+
+    public bool HitTest(Point pt)
+    {
+        foreach (var op in this)
+            if (op.Item.HitTest(pt))
+                return true;
+        return false;
     }
 }
 

+ 37 - 0
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs

@@ -0,0 +1,37 @@
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.Composition.Drawing;
+
+internal class CompositionDrawListSceneBrushContent : ISceneBrushContent
+{
+    private readonly CompositionDrawList _drawList;
+
+    public CompositionDrawListSceneBrushContent(ImmutableTileBrush brush, CompositionDrawList drawList, Rect rect, bool useScalableRasterization)
+    {
+        Brush = brush;
+        Rect = rect;
+        UseScalableRasterization = useScalableRasterization;
+        _drawList = drawList;
+    }
+
+    public ITileBrush Brush { get; }
+    public Rect Rect { get; }
+    
+    public double Opacity => Brush.Opacity;
+    public ITransform? Transform => Brush.Transform;
+    public RelativePoint TransformOrigin => Brush.TransformOrigin;
+    
+    public void Dispose() => _drawList.Dispose();
+
+    public void Render(IDrawingContextImpl context, Matrix? transform)
+    {
+        if (transform.HasValue)
+            _drawList.Render(context, transform.Value);
+        else
+            _drawList.Render(context);
+    }
+
+    public bool UseScalableRasterization { get; }
+}

+ 89 - 94
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Numerics;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
@@ -7,7 +8,7 @@ using Avalonia.Platform;
 using Avalonia.Rendering.Composition.Drawing;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Utilities;
-using Avalonia.VisualTree;
+using Avalonia.Threading;
 
 // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
 
@@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition;
 /// <summary>
 /// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
 /// </summary>
-internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
+internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
 {
     private CompositionDrawListBuilder _builder = new();
     private int _drawOperationIndex;
+    
+    private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
+        ThreadSafeObjectPool<Stack<Matrix>>.Default;
 
-    /// <inheritdoc/>
-    public Matrix Transform { get; set; } = Matrix.Identity;
+    private Stack<Matrix>? _transforms;
 
-    /// <inheritdoc/>
-    public void Clear(Color color)
-    {
-        // Cannot clear a deferred scene.
-    }
+    private static ThreadSafeObjectPool<Stack<bool>> OpacityMaskPopStackPool { get; } =
+        ThreadSafeObjectPool<Stack<bool>>.Default;
 
-    /// <inheritdoc/>
-    public void Dispose()
-    {
-        // Nothing to do here since we allocate no unmanaged resources.
-    }
+    private Stack<bool>? _needsToPopOpacityMask;
 
+    public Matrix Transform { get; set; } = Matrix.Identity;
+    
     public void BeginUpdate(CompositionDrawList? list)
     {
         _builder.Reset(list);
         _drawOperationIndex = 0;
     }
 
-    public CompositionDrawList EndUpdate()
+    public CompositionDrawList? EndUpdate()
     {
+        // Make sure that any pending pop operations are completed
+        Dispose();
+        
         _builder.TrimTo(_drawOperationIndex);
-        return _builder.DrawOperations!;
+        return _builder.DrawOperations;
     }
+    
+    protected override void DisposeCore()
+    {
+        if (_transforms != null)
+        {
+            _transforms.Clear();
+            TransformStackPool.ReturnAndSetNull(ref _transforms);
+        }
 
-    /// <inheritdoc/>
-    public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
+        if (_needsToPopOpacityMask != null)
+        {
+            _needsToPopOpacityMask.Clear();
+            _needsToPopOpacityMask = null;
+        }
+    }
+    
+    protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
     {
         var next = NextDrawAs<GeometryNode>();
 
         if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
         {
-            Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
+            Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry));
         }
         else
         {
@@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
-        BitmapInterpolationMode bitmapInterpolationMode)
+    internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
+        BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
     {
         var next = NextDrawAs<ImageNode>();
 
@@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
     }
 
     /// <inheritdoc/>
-    public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
-    {
-        // This method is currently only used to composite layers so shouldn't be called here.
-        throw new NotSupportedException();
-    }
-
-    /// <inheritdoc/>
-    public void DrawLine(IPen? pen, Point p1, Point p2)
+    protected override void DrawLineCore(IPen? pen, Point p1, Point p2)
     {
         if (pen is null)
         {
@@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
 
         if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
         {
-            Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
+            Add(new LineNode(Transform, pen, p1, p2));
         }
         else
         {
@@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
     }
 
     /// <inheritdoc/>
-    public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
+    protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect,
         BoxShadows boxShadows = default)
     {
         var next = NextDrawAs<RectangleNode>();
 
         if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
         {
-            Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
+            Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows));
         }
         else
         {
@@ -138,21 +145,21 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
+    protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
     {
         var next = NextDrawAs<EllipseNode>();
 
         if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
         {
-            Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush)));
+            Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect));
         }
         else
         {
             ++_drawOperationIndex;
         }
     }
-
-    public void Custom(ICustomDrawOperation custom)
+    
+    public override void Custom(ICustomDrawOperation custom)
     {
         var next = NextDrawAs<CustomDrawOperation>();
         if (next == null || !next.Item.Equals(Transform, custom))
@@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
             ++_drawOperationIndex;
     }
 
-    public object? GetFeature(Type t) => null;
-
-    /// <inheritdoc/>
-    public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
+    public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
     {
         if (foreground is null)
         {
@@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
 
         var next = NextDrawAs<GlyphRunNode>();
 
-        if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
+        if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl))
         {
-            Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground)));
+            Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl));
         }
 
         else
@@ -184,13 +188,17 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    public IDrawingContextLayerImpl CreateLayer(Size size)
+    protected override void PushTransformCore(Matrix matrix)
     {
-        throw new NotSupportedException("Creating layers on a deferred drawing context not supported");
+        _transforms ??= TransformStackPool.Get();
+        _transforms.Push(Transform);
+        Transform = matrix * Transform;
     }
+    
+    protected override void PopTransformCore() =>
+        Transform = (_transforms ?? throw new InvalidOperationException()).Pop();
 
-    /// <inheritdoc/>
-    public void PopClip()
+    protected override void PopClipCore()
     {
         var next = NextDrawAs<ClipNode>();
 
@@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
     }
 
     /// <inheritdoc/>
-    public void PopGeometryClip()
+    protected override void PopGeometryClipCore()
     {
         var next = NextDrawAs<GeometryClipNode>();
 
@@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void PopBitmapBlendMode()
+    protected override void PopBitmapBlendModeCore()
     {
         var next = NextDrawAs<BitmapBlendModeNode>();
 
@@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void PopOpacity()
+    protected override void PopOpacityCore()
     {
         var next = NextDrawAs<OpacityNode>();
 
@@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void PopOpacityMask()
+    protected override void PopOpacityMaskCore()
     {
+        if (!_needsToPopOpacityMask!.Pop())
+            return;
+        
         var next = NextDrawAs<OpacityMaskNode>();
 
         if (next == null || !next.Item.Equals(null, null))
         {
-            Add(new OpacityMaskNode());
+            Add(new OpacityMaskPopNode());
         }
         else
         {
@@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void PushClip(Rect clip)
+
+    protected override void PushClipCore(Rect clip)
     {
         var next = NextDrawAs<ClipNode>();
 
@@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc />
-    public void PushClip(RoundedRect clip)
+    protected override void PushClipCore(RoundedRect clip)
     {
         var next = NextDrawAs<ClipNode>();
 
@@ -294,26 +301,24 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void PushGeometryClip(IGeometryImpl? clip)
+    protected override void PushGeometryClipCore(Geometry clip)
     {
-        if (clip is null)
+        if (clip.PlatformImpl is null)
             return;
 
         var next = NextDrawAs<GeometryClipNode>();
 
-        if (next == null || !next.Item.Equals(Transform, clip))
+        if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl))
         {
-            Add(new GeometryClipNode(Transform, clip));
+            Add(new GeometryClipNode(Transform, clip.PlatformImpl));
         }
         else
         {
             ++_drawOperationIndex;
         }
     }
-
-    /// <inheritdoc/>
-    public void PushOpacity(double opacity, Rect bounds)
+    
+    protected override void PushOpacityCore(double opacity, Rect bounds)
     {
         var next = NextDrawAs<OpacityNode>();
 
@@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
         }
     }
 
-    /// <inheritdoc/>
-    public void PushOpacityMask(IBrush mask, Rect bounds)
+    protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
     {
         var next = NextDrawAs<OpacityMaskNode>();
 
+        bool needsToPop = true;
         if (next == null || !next.Item.Equals(mask, bounds))
         {
-            Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
+            var immutableMask = ConvertBrush(mask);
+            if (immutableMask != null)
+                Add(new OpacityMaskNode(immutableMask, bounds));
+            else
+                needsToPop = false;
         }
         else
         {
             ++_drawOperationIndex;
         }
+
+        _needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get();
+        _needsToPopOpacityMask.Push(needsToPop);
     }
 
     /// <inheritdoc/>
-    public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+    protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
     {
         var next = NextDrawAs<BitmapBlendModeNode>();
 
@@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
             : null;
     }
     
-    private static IDisposable? CreateChildScene(IBrush? brush)
+    private IImmutableBrush? ConvertBrush(IBrush? brush)
     {
-        if (brush is VisualBrush visualBrush)
-        {
-            var visual = visualBrush.Visual;
-
-            if (visual != null)
-            {
-                // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer
-                // We should directly reference the corresponding CompositionVisual (which should
-                // be attached to the same composition target) like UWP does.
-                // Render-able visuals shouldn't be dangling unattached
-                (visual as IVisualBrushInitialize)?.EnsureInitialized();
-                
-                var recorder = new CompositionDrawingContext();
-                recorder.BeginUpdate(null);
-                ImmediateRenderer.Render(visual, new DrawingContext(recorder));
-                var drawList = recorder.EndUpdate();
-                drawList.Size = visual.Bounds.Size;
-
-                return drawList;
-            }
-        }
-        return null;
+        if (brush is IMutableBrush mutable)
+            return mutable.ToImmutable();
+        if (brush is ISceneBrush sceneBrush)
+            return sceneBrush.CreateContent();
+        return (IImmutableBrush?)brush;
     }
 }

+ 2 - 28
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server;
 internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
 {
     private IDrawingContextImpl _impl;
-    private readonly VisualBrushRenderer _visualBrushRenderer;
 
-    public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer)
+    public CompositorDrawingContextProxy(IDrawingContextImpl impl)
     {
         _impl = impl;
-        _visualBrushRenderer = visualBrushRenderer;
-    }
-
-    // This is a hack to make it work with the current way of handling visual brushes
-    public CompositionDrawList? VisualBrushDrawList
-    {
-        get => _visualBrushRenderer.VisualBrushDrawList;
-        set => _visualBrushRenderer.VisualBrushDrawList = value;
     }
     
     public Matrix PostTransform { get; set; } = Matrix.Identity;
@@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
     }
 
     public object? GetFeature(Type t) => _impl.GetFeature(t);
-
-    public class VisualBrushRenderer : IVisualBrushRenderer
-    {
-        public CompositionDrawList? VisualBrushDrawList { get; set; }
-        public Size GetRenderTargetSize(IVisualBrush brush)
-        {
-            return VisualBrushDrawList?.Size ?? default;
-        }
-
-        public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
-        {
-            if (VisualBrushDrawList != null)
-            {
-                foreach (var cmd in VisualBrushDrawList)
-                    cmd.Item.Render(context);
-            }
-        }
-    }
+    
 
     public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
     {

+ 3 - 4
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server
             Readback.CompleteWrite(Revision);
 
             _redrawRequested = false;
-            using (var targetContext = _renderTarget.CreateDrawingContext(null))
+            using (var targetContext = _renderTarget.CreateDrawingContext())
             {
                 var layerSize = Size * Scaling;
                 if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
@@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server
 
                 if (!_dirtyRect.IsDefault)
                 {
-                    var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer();
-                    using (var context = _layer.CreateDrawingContext(visualBrushHelper))
+                    using (var context = _layer.CreateDrawingContext())
                     {
                         context.PushClip(_dirtyRect);
                         context.Clear(Colors.Transparent);
-                        Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect);
+                        Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect);
                         context.PopClip();
                     }
                 }

+ 0 - 29
src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs

@@ -1,29 +0,0 @@
-using Avalonia.Media;
-using Avalonia.Metadata;
-using Avalonia.Platform;
-
-namespace Avalonia.Rendering
-{
-    /// <summary>
-    /// Defines a renderer used to render a visual brush to a bitmap.
-    /// </summary>
-    [Unstable]
-    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);
-    }
-}

+ 2 - 35
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@@ -14,19 +14,8 @@ namespace Avalonia.Rendering
     /// a simple tree traversal.
     /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
     /// </summary>
-    internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer
+    internal class ImmediateRenderer
     {
-        /// <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(Visual visual, IRenderTarget target)
-        {
-            using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer()));
-            Render(context, visual, visual.Bounds);
-        }
-
         /// <summary>
         /// Renders a visual to a drawing context.
         /// </summary>
@@ -36,28 +25,6 @@ namespace Avalonia.Rendering
         {
             Render(context, visual, visual.Bounds);
         }
-        
-
-        /// <inheritdoc/>
-        Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
-        {
-            (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized();
-            return brush.Visual?.Bounds.Size ?? default;
-        }
-
-        /// <inheritdoc/>
-        void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
-        {
-            if (brush.Visual is { } visual)
-            {
-                Render(new DrawingContext(context), visual, visual.Bounds);
-            }
-        }
-
-        internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
-        {
-            Render(context, visual, visual.Bounds);
-        }
 
         private static Rect GetTransformedBounds(Visual visual)
         {
@@ -75,7 +42,7 @@ namespace Avalonia.Rendering
         }
 
 
-        private static void Render(DrawingContext context, Visual visual, Rect clipRect)
+        public static void Render(DrawingContext context, Visual visual, Rect clipRect)
         {
             var opacity = visual.Opacity;
             var clipToBounds = visual.ClipToBounds;

+ 6 - 9
src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs

@@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// Base class for draw operations that can use a brush.
     /// </summary>
-    internal abstract class BrushDrawOperation : DrawOperation
+    internal abstract class BrushDrawOperation : DrawOperationWithTransform
     {
-        public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux)
+        public IImmutableBrush? Brush { get; }
+
+        public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush)
             : base(bounds, transform)
         {
-            Aux = aux;
+            Brush = brush;
         }
 
-        /// <summary>
-        /// Auxiliary data required to draw the brush
-        /// </summary>
-        public IDisposable? Aux { get; }
-
         public override void Dispose()
         {
-            Aux?.Dispose();
+            (Brush as ISceneBrushContent)?.Dispose();
             base.Dispose();
         }
     }

+ 1 - 3
src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// A node in the scene graph which represents a clip push or pop.
     /// </summary>
-    internal class ClipNode : IDrawOperation
+    internal class ClipNode : IDrawOperationWithTransform
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
@@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
-
             if (Clip.HasValue)
             {
                 context.PushClip(Clip.Value);

+ 24 - 15
src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs

@@ -4,30 +4,19 @@ using Avalonia.Platform;
 
 namespace Avalonia.Rendering.SceneGraph
 {
-    internal sealed class CustomDrawOperation : DrawOperation
+    internal sealed class CustomDrawOperation : DrawOperationWithTransform
     {
-        public Matrix Transform { get; }
         public ICustomDrawOperation Custom { get; }
         public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) 
             : base(custom.Bounds, transform)
         {
-            Transform = transform;
             Custom = custom;
         }
 
-        public override bool HitTest(Point p)
-        {
-            if (Transform.HasInverse)
-            {
-                return Custom.HitTest(p * Transform.Invert());
-            }
-
-            return false;
-        }
+        public override bool HitTest(Point p) => Custom.HitTest(p);
 
         public override void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
             Custom.Render(context);
         }
 
@@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph
             Transform == transform && Custom?.Equals(custom) == true;
     }
 
-    public interface ICustomDrawOperation : IDrawOperation, IEquatable<ICustomDrawOperation>
+    public interface ICustomDrawOperation : IEquatable<ICustomDrawOperation>, IDisposable
     {
-        
+        /// <summary>
+        /// Gets the bounds of the visible content in the node in global coordinates.
+        /// </summary>
+        Rect Bounds { get; }
+
+        /// <summary>
+        /// Hit test the geometry in this node.
+        /// </summary>
+        /// <param name="p">The point in global coordinates.</param>
+        /// <returns>True if the point hits the node's geometry; otherwise false.</returns>
+        /// <remarks>
+        /// This method does not recurse to childs, if you want
+        /// to hit test children they must be hit tested manually.
+        /// </remarks>
+        bool HitTest(Point p);
+
+        /// <summary>
+        /// Renders the node to a drawing context.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        void Render(IDrawingContextImpl context);
     }
 }

+ 10 - 0
src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs

@@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph
         {
         }
     }
+
+    internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform
+    {
+        protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform)
+        {
+            Transform = transform;
+        }
+
+        public Matrix Transform { get; }
+    }
 }

+ 9 - 28
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph
     {
         public EllipseNode(
             Matrix transform, 
-            IBrush? brush, 
+            IImmutableBrush? brush, 
             IPen? pen, 
-            Rect rect, 
-            IDisposable? aux = null) 
-            : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux)
+            Rect rect) 
+            : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush)
         {
-            Transform = transform;
-            Brush = brush?.ToImmutable();
             Pen = pen?.ToImmutable();
             Rect = rect;
         }
 
-        /// <summary>
-        /// Gets the fill brush.
-        /// </summary>
-        public IBrush? Brush { get; }
-
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
         public ImmutablePen? Pen { get; }
 
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
-
         /// <summary>
         /// Gets the rect of the ellipse to draw.
         /// </summary>
@@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph
                    rect.Equals(Rect);
         }
 
-        public override void Render(IDrawingContextImpl context)
-        {
-            context.Transform = Transform;
-            context.DrawEllipse(Brush, Pen, Rect);
-        }
+        public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect);
 
         public override bool HitTest(Point p)
         {
-            if (!Transform.TryInvert(out Matrix inverted))
-            {
-                return false;
-            }
-
-            p *= inverted;
-
             var center = Rect.Center;
 
             var strokeThickness = Pen?.Thickness ?? 0;
@@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph
 
             return false;
         }
+
+        public override void Dispose()
+        {
+            (Brush as ISceneBrushContent)?.Dispose();
+        }
     }
 }

+ 2 - 22
src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// A node in the scene graph which represents a rectangle draw.
     /// </summary>
-    internal class ExperimentalAcrylicNode : DrawOperation
+    internal class ExperimentalAcrylicNode : DrawOperationWithTransform
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="RectangleNode"/> class.
@@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph
             RoundedRect rect)
             : base(rect.Rect, transform)
         {
-            Transform = transform;
             Material = material.ToImmutable();
             Rect = rect;
         }
 
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
-
         public IExperimentalAcrylicMaterial Material { get; }        
 
         /// <summary>
@@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public override void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
-
             if(context is IDrawingContextWithAcrylicLikeSupport idc)
             {
                 idc.DrawRectangle(Material, Rect);
@@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p)
-        {
-            // TODO: This doesn't respect CornerRadius yet.
-            if (Transform.HasInverse)
-            {
-                p *= Transform.Invert();
-
-                var rect = Rect.Rect;
-                return rect.ContainsExclusive(p);
-            }
-
-            return false;
-        }
+        public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p);
     }
 }

+ 1 - 3
src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// A node in the scene graph which represents a geometry clip push or pop.
     /// </summary>
-    internal class GeometryClipNode : IDrawOperation
+    internal class GeometryClipNode : IDrawOperationWithTransform
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
@@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
-
             if (Clip != null)
             {
                 context.PushGeometryClip(Clip);

+ 5 - 25
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="geometry">The geometry.</param>
         /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public GeometryNode(Matrix transform,
-            IBrush? brush,
+            IImmutableBrush? brush,
             IPen? pen,
-            IGeometryImpl geometry,
-            IDisposable? aux)
-            : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux)
+            IGeometryImpl geometry)
+            : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush)
         {
-            Transform = transform;
-            Brush = brush?.ToImmutable();
             Pen = pen?.ToImmutable();
             Geometry = geometry;
         }
 
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
-
-        /// <summary>
-        /// Gets the fill brush.
-        /// </summary>
-        public IBrush? Brush { get; }
-
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
@@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public override void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
             context.DrawGeometry(Brush, Pen, Geometry);
         }
 
         /// <inheritdoc/>
         public override bool HitTest(Point p)
         {
-            if (Transform.HasInverse)
-            {
-                p *= Transform.Invert();
-                return (Brush != null && Geometry.FillContains(p)) ||
-                    (Pen != null && Geometry.StrokeContains(Pen, p));
-            }
-
-            return false;
+            return (Brush != null && Geometry.FillContains(p)) ||
+                   (Pen != null && Geometry.StrokeContains(Pen, p));
         }
     }
 }

+ 9 - 24
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public GlyphRunNode(
             Matrix transform,
-            IBrush foreground,
-            IRef<IGlyphRunImpl> glyphRun,
-            IDisposable? aux = null)
-            : base(new Rect(glyphRun.Item.Size), transform, aux)
+            IImmutableBrush foreground,
+            IRef<IGlyphRunImpl> glyphRun)
+            : base(new Rect(glyphRun.Item.Size), transform, foreground)
         {
-            Transform = transform;
-            Foreground = foreground.ToImmutable();
             GlyphRun = glyphRun.Clone();
         }
-
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
-
-        /// <summary>
-        /// Gets the foreground brush.
-        /// </summary>
-        public IBrush Foreground { get; }
-
+        
+        
         /// <summary>
         /// Gets the glyph run to draw.
         /// </summary>
         public IRef<IGlyphRunImpl> GlyphRun { get; }
 
         /// <inheritdoc/>
-        public override void Render(IDrawingContextImpl context)
-        {
-            context.Transform = Transform;
-            context.DrawGlyphRun(Foreground, GlyphRun);
-        }
+        public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun);
 
         /// <summary>
         /// Determines if this draw operation equals another.
@@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph
         internal bool Equals(Matrix transform, IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
         {
             return transform == Transform &&
-                   Equals(foreground, Foreground) &&
+                   Equals(foreground, Brush) &&
                    Equals(glyphRun.Item, GlyphRun.Item);
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
+        public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p);
 
         public override void Dispose()
         {
             GlyphRun?.Dispose();
+            base.Dispose();
         }
     }
 }

+ 9 - 1
src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// Represents a node in the low-level scene graph that represents geometry.
     /// </summary>
-    public interface IDrawOperation : IDisposable
+    internal interface IDrawOperation : IDisposable
     {
         /// <summary>
         /// Gets the bounds of the visible content in the node in global coordinates.
@@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="context">The drawing context.</param>
         void Render(IDrawingContextImpl context);
     }
+
+    internal interface IDrawOperationWithTransform : IDrawOperation
+    {
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        Matrix Transform { get; }
+    }
 }

+ 3 - 18
src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs

@@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// A node in the scene graph which represents an image draw.
     /// </summary>
-    internal class ImageNode : DrawOperation
+    internal class ImageNode : DrawOperationWithTransform
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageNode"/> class.
@@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph
         public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
             : base(destRect, transform)
         {
-            Transform = transform;
             Source = source.Clone();
             Opacity = opacity;
             SourceRect = sourceRect;
             DestRect = destRect;
             BitmapInterpolationMode = bitmapInterpolationMode;
             SourceVersion = Source.Item.Version;
-        }        
-
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
+        }
 
         /// <summary>
         /// Gets the image to draw.
@@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph
         /// </value>
         public BitmapInterpolationMode BitmapInterpolationMode { get; }
         
-        /// <summary>
-        /// The bitmap blending mode.
-        /// </summary>
-        /// <value>
-        /// The blending mode.
-        /// </value>
-        public BitmapBlendingMode BitmapBlendingMode { get; }
-
         /// <summary>
         /// Determines if this draw operation equals another.
         /// </summary>
@@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public override void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
             context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
+        public override bool HitTest(Point p) => DestRect.ContainsExclusive(p);
 
         public override void Dispose()
         {

+ 3 - 16
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// <summary>
     /// A node in the scene graph which represents a line draw.
     /// </summary>
-    internal class LineNode : BrushDrawOperation
+    internal class LineNode : DrawOperationWithTransform
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="GeometryNode"/> class.
@@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph
             Matrix transform,
             IPen pen,
             Point p1,
-            Point p2,
-            IDisposable? aux = null)
-            : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux)
+            Point p2)
+            : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
         {
-            Transform = transform;
             Pen = pen.ToImmutable();
             P1 = p1;
             P2 = p2;
         }
 
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
-
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
@@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph
 
         public override void Render(IDrawingContextImpl context)
         {
-            context.Transform = Transform;
             context.DrawLine(Pen, P1, P2);
         }
 
         public override bool HitTest(Point p)
         {
-            if (!Transform.HasInverse)
-                return false;
-
-            p *= Transform.Invert();
-
             var halfThickness = Pen.Thickness / 2;
             var minX = Math.Min(P1.X, P2.X) - halfThickness;
             var maxX = Math.Max(P1.X, P2.X) + halfThickness;

+ 15 - 26
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="mask">The opacity mask to push.</param>
         /// <param name="bounds">The bounds of the mask.</param>
         /// <param name="aux">Auxiliary data required to draw the brush.</param>
-        public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null)
-            : base(default, Matrix.Identity, aux)
+        public OpacityMaskNode(IImmutableBrush mask, Rect bounds)
+            : base(default, Matrix.Identity, mask)
         {
-            Mask = mask.ToImmutable();
             MaskBounds = bounds;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
-        /// opacity mask pop.
-        /// </summary>
-        public OpacityMaskNode()
-            : base(default, Matrix.Identity, null)
-        {
-        }
-
-        /// <summary>
-        /// Gets the mask to be pushed or null if the operation represents a pop.
-        /// </summary>
-        public IBrush? Mask { get; }
-
         /// <summary>
         /// Gets the bounds of the opacity mask or null if the operation represents a pop.
         /// </summary>
@@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds;
+        public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds;
 
         /// <inheritdoc/>
         public override void Render(IDrawingContextImpl context)
         {
-            if (Mask != null)
-            {
-                context.PushOpacityMask(Mask, MaskBounds!.Value);
-            }
-            else
-            {
-                context.PopOpacityMask();
-            }
+            context.PushOpacityMask(Brush!, MaskBounds!.Value);
         }
     }
+
+    internal class OpacityMaskPopNode : DrawOperation
+    {
+        public OpacityMaskPopNode() : base(default, Matrix.Identity)
+        {
+        }
+
+        public override bool HitTest(Point p) => false;
+
+        public override void Render(IDrawingContextImpl context) => context.PopOpacityMask();
+    }
 }

+ 13 - 39
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public RectangleNode(
             Matrix transform,
-            IBrush? brush,
+            IImmutableBrush? brush,
             IPen? pen,
             RoundedRect rect,
-            BoxShadows boxShadows,
-            IDisposable? aux = null)
-            : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux)
+            BoxShadows boxShadows)
+            : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush)
         {
-            Transform = transform;
-            Brush = brush?.ToImmutable();
             Pen = pen?.ToImmutable();
             Rect = rect;
             BoxShadows = boxShadows;
         }
 
-        /// <summary>
-        /// Gets the transform with which the node will be drawn.
-        /// </summary>
-        public Matrix Transform { get; }
-
-        /// <summary>
-        /// Gets the fill brush.
-        /// </summary>
-        public IBrush? Brush { get; }
-
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
@@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public override void Render(IDrawingContextImpl context)
-        {
-            context.Transform = Transform;
-
-            context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
-        }
+        public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
 
         /// <inheritdoc/>
         public override bool HitTest(Point p)
         {
-            // TODO: This doesn't respect CornerRadius yet.
-            if (Transform.HasInverse)
+            if (Brush != null)
             {
-                p *= Transform.Invert();
-
-                if (Brush != null)
-                {
-                    var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
-                    return rect.ContainsExclusive(p);
-                }
-                else
-                {
-                    var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
-                    var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
-                    return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
-                }
+                var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
+                return rect.ContainsExclusive(p);
+            }
+            else
+            {
+                var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
+                var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
+                return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
             }
-
-            return false;
         }
     }
 }

+ 5 - 2
src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs

@@ -2,7 +2,7 @@ using System.Collections.Generic;
 
 namespace Avalonia.Threading
 {
-    public class ThreadSafeObjectPool<T> where T : class, new()
+    internal class ThreadSafeObjectPool<T> where T : class, new()
     {
         private Stack<T> _stack = new Stack<T>();
         public static ThreadSafeObjectPool<T> Default { get; } = new ThreadSafeObjectPool<T>();
@@ -17,11 +17,14 @@ namespace Avalonia.Threading
             }
         }
 
-        public void Return(T obj)
+        public void ReturnAndSetNull(ref T? obj)
         {
+            if (obj == null)
+                return;
             lock (_stack)
             {
                 _stack.Push(obj);
+                obj = null;
             }
         }
     }

+ 1 - 1
src/Avalonia.Controls/ExperimentalAcrylicBorder.cs

@@ -82,7 +82,7 @@ namespace Avalonia.Controls
 
         public sealed override void Render(DrawingContext context)
         {
-            if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc)
+            if (context is IDrawingContextWithAcrylicLikeSupport idc)
             {
                 var cornerRadius = CornerRadius;
 

+ 1 - 1
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -148,7 +148,7 @@ namespace Avalonia.Controls.Utils
                 var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight,
                     _cornerRadius.BottomRight, _cornerRadius.BottomLeft);
 
-                context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows);
+                context.DrawRectangle(background, pen, rrect, boxShadows);
             }
         }
 

+ 2 - 2
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -325,7 +325,7 @@ namespace Avalonia.Headless
 
             }
 
-            public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+            public IDrawingContextImpl CreateDrawingContext()
             {
                 return new HeadlessDrawingContextStub();
             }
@@ -491,7 +491,7 @@ namespace Avalonia.Headless
 
             }
 
-            public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+            public IDrawingContextImpl CreateDrawingContext()
             {
                 return new HeadlessDrawingContextStub();
             }

+ 1 - 1
src/Avalonia.X11/X11CursorFactory.cs

@@ -115,7 +115,7 @@ namespace Avalonia.X11
                
                 using (var cpuContext = platformRenderInterface.CreateBackendContext(null))
                 using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this }))
-                using (var ctx = renderTarget.CreateDrawingContext(null))
+                using (var ctx = renderTarget.CreateDrawingContext())
                 {
                     var r = new Rect(_pixelSize.ToSize(1)); 
                     ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r);

+ 1 - 1
src/Avalonia.X11/X11IconLoader.cs

@@ -43,7 +43,7 @@ namespace Avalonia.X11
             _bdata = new uint[_width * _height];
             using(var cpuContext = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>().CreateBackendContext(null))
             using(var rt = cpuContext.CreateRenderTarget(new[]{this}))
-            using (var ctx = rt.CreateDrawingContext(null))
+            using (var ctx = rt.CreateDrawingContext())
                 ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size),
                     new Rect(0, 0, _width, _height));
             Data = new UIntPtr[_width * _height + 2];

+ 103 - 33
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -10,7 +10,9 @@ using Avalonia.Rendering.SceneGraph;
 using Avalonia.Rendering.Utilities;
 using Avalonia.Utilities;
 using Avalonia.Media.Imaging;
+using Avalonia.Skia.Helpers;
 using SkiaSharp;
+using ISceneBrush = Avalonia.Media.ISceneBrush;
 
 namespace Avalonia.Skia
 {
@@ -25,7 +27,6 @@ namespace Avalonia.Skia
         private readonly Stack<double> _opacityStack = new();
         private readonly Stack<BitmapBlendingMode> _blendingModeStack = new();
         private readonly Matrix? _postTransform;
-        private readonly IVisualBrushRenderer? _visualBrushRenderer;
         private double _currentOpacity = 1.0f;
         private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver;
         private readonly bool _canTextUseLcdRendering;
@@ -61,12 +62,7 @@ namespace Avalonia.Skia
             /// Dpi of drawings.
             /// </summary>
             public Vector Dpi;
-
-            /// <summary>
-            /// Visual brush renderer.
-            /// </summary>
-            public IVisualBrushRenderer? VisualBrushRenderer;
-
+            
             /// <summary>
             /// Render text without Lcd rendering.
             /// </summary>
@@ -141,7 +137,6 @@ namespace Avalonia.Skia
                 ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
 
             _dpi = createInfo.Dpi;
-            _visualBrushRenderer = createInfo.VisualBrushRenderer;
             _disposables = disposables;
             _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering;
             _grContext = createInfo.GrContext;
@@ -908,7 +903,7 @@ namespace Avalonia.Skia
 
             paintWrapper.AddDisposable(intermediate);
 
-            using (var context = intermediate.CreateDrawingContext(null))
+            using (var context = intermediate.CreateDrawingContext())
             {
                 var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96));
                 var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi));
@@ -970,36 +965,98 @@ namespace Avalonia.Skia
             }
         }
 
-        /// <summary>
-        /// Configure paint wrapper to use visual brush.
-        /// </summary>
-        /// <param name="paintWrapper">Paint wrapper.</param>
-        /// <param name="visualBrush">Visual brush.</param>
-        /// <param name="visualBrushRenderer">Visual brush renderer.</param>
-        /// <param name="tileBrushImage">Tile brush image.</param>
-        private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush,
-            IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage)
+        private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content,
+            Size targetSize)
         {
-            if (visualBrushRenderer == null)
-            {
-                throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
-            }
-
-            var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
+            if(content.UseScalableRasterization)
+                ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize);
+            else
+                ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize);
+        }
+        
+        private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content,
+            Size targetSize)
+        {
+            var rect = content.Rect;
+            var intermediateSize = rect.Size;
 
             if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
             {
-                var intermediate = CreateRenderTarget(intermediateSize, false);
+                using var intermediate = CreateRenderTarget(intermediateSize, false);
 
-                using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer))
+                using (var ctx = intermediate.CreateDrawingContext())
                 {
                     ctx.Clear(Colors.Transparent);
-
-                    visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
+                    content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y));
                 }
 
-                tileBrushImage = intermediate;
-                paintWrapper.AddDisposable(tileBrushImage);
+                ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate);
+            }
+        }
+        
+        private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content,
+            Size targetSize)
+        {
+            var rect = content.Rect;
+            var contentSize = rect.Size;
+            if (contentSize.Width <= 0 || contentSize.Height <= 0)
+            {
+                paintWrapper.Paint.Color = SKColor.Empty;
+                return;
+            }
+            
+            var tileBrush = content.Brush;
+            var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y);
+
+            var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize);
+            transform *= calc.IntermediateTransform;
+            
+            using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi);
+            using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize))
+            {
+                ctx.PushClip(calc.IntermediateClip);
+                content.Render(ctx, transform);
+                ctx.PopClip();
+            }
+
+            using var picture = pictureTarget.GetPicture();
+
+            var paintTransform =
+                tileBrush.TileMode != TileMode.None
+                    ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
+                    : SKMatrix.CreateIdentity();
+
+            SKShaderTileMode tileX =
+                tileBrush.TileMode == TileMode.None
+                    ? SKShaderTileMode.Clamp
+                    : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
+                        ? SKShaderTileMode.Mirror
+                        : SKShaderTileMode.Repeat;
+
+            SKShaderTileMode tileY =
+                tileBrush.TileMode == TileMode.None
+                    ? SKShaderTileMode.Clamp
+                    : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
+                        ? SKShaderTileMode.Mirror
+                        : SKShaderTileMode.Repeat;
+
+            paintTransform = SKMatrix.Concat(paintTransform,
+                SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
+            
+            if (tileBrush.Transform is { })
+            {
+                var origin = tileBrush.TransformOrigin.ToPixels(targetSize);
+                var offset = Matrix.CreateTranslation(origin);
+                var brushTransform = (-offset) * tileBrush.Transform.Value * (offset);
+
+                paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix());
+            }
+
+            using (var shader = picture.ToShader(tileX, tileY, paintTransform,
+                       new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height)))
+            {
+                paintWrapper.Paint.FilterQuality = SKFilterQuality.None;
+                paintWrapper.Paint.Shader = shader;
             }
         }
 
@@ -1113,12 +1170,25 @@ namespace Avalonia.Skia
             }
 
             var tileBrush = brush as ITileBrush;
-            var visualBrush = brush as IVisualBrush;
             var tileBrushImage = default(IDrawableBitmapImpl);
 
-            if (visualBrush != null)
+            if (brush is ISceneBrush sceneBrush)
             {
-                ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage);
+                using (var content = sceneBrush.CreateContent())
+                {
+                    if (content != null)
+                    {
+                        ConfigureSceneBrushContent(ref paintWrapper, content, targetSize);
+                        return paintWrapper;
+                    }
+                    else
+                        paint.Color = default;
+                }
+            }
+            else if (brush is ISceneBrushContent sceneBrushContent)
+            {
+                ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize);
+                return paintWrapper;
             }
             else
             {

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

@@ -36,7 +36,7 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
             var framebuffer = _platformSurface.Lock();
             var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height,
@@ -55,7 +55,6 @@ namespace Avalonia.Skia
             {
                 Surface = _framebufferSurface,
                 Dpi = framebuffer.Dpi,
-                VisualBrushRenderer = visualBrushRenderer,
                 DisableTextLcdRendering = true
             };
 

+ 1 - 2
src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Skia
             _renderTarget.Dispose();
         }
 
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
             var session = _renderTarget.BeginRenderingSession();
 
@@ -31,7 +31,6 @@ namespace Avalonia.Skia
                 GrContext = session.GrContext,
                 Surface = session.SkSurface,
                 Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor,
-                VisualBrushRenderer = visualBrushRenderer,
                 DisableTextLcdRendering = true,
                 Gpu = _skiaGpu,
                 CurrentSession =  session

+ 1 - 2
src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs

@@ -15,13 +15,12 @@ namespace Avalonia.Skia.Helpers
         /// <param name="dpi"></param>
         /// <param name="visualBrushRenderer"></param>
         /// <returns>DrawingContext</returns>
-        public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer? visualBrushRenderer = null)
+        public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi)
         {
             var createInfo = new DrawingContextImpl.CreateInfo
             {
                 Canvas = canvas,
                 Dpi = dpi,
-                VisualBrushRenderer = visualBrushRenderer,
                 DisableTextLcdRendering = true,
             };
 

+ 10 - 0
src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs

@@ -60,5 +60,15 @@ namespace Avalonia.Skia.Helpers
                 }
             }
         }
+
+        // This method is here mostly for debugging purposes
+        internal static void SavePicture(SKPicture picture, float scale, string path)
+        {
+            var snapshotSize = new SKSizeI((int)Math.Ceiling(picture.CullRect.Width * scale),
+                (int)Math.Ceiling(picture.CullRect.Height * scale));
+            using var snap =
+                SKImage.FromPicture(picture, snapshotSize, SKMatrix.CreateScale(scale, scale));
+            SaveImage(snap, path);
+        }
     }
 }

+ 55 - 0
src/Skia/Avalonia.Skia/PictureRenderTarget.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Platform;
+using Avalonia.Reactive;
+using SkiaSharp;
+
+namespace Avalonia.Skia;
+
+internal class PictureRenderTarget : IDisposable
+{
+    private readonly ISkiaGpu? _gpu;
+    private readonly GRContext? _grContext;
+    private readonly Vector _dpi;
+    private SKPicture? _picture;
+
+    public PictureRenderTarget(ISkiaGpu? gpu, GRContext? grContext, Vector dpi)
+    {
+        _gpu = gpu;
+        _grContext = grContext;
+        _dpi = dpi;
+    }
+
+    public SKPicture GetPicture()
+    {
+        var rv = _picture ?? throw new InvalidOperationException();
+        _picture = null;
+        return rv;
+    }
+
+    public IDrawingContextImpl CreateDrawingContext(Size size)
+    {
+        var recorder = new SKPictureRecorder();
+        var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96),
+            (float)(size.Height * _dpi.Y / 96)));
+        
+        canvas.RestoreToCount(-1);
+        canvas.ResetMatrix();
+            
+        var createInfo = new DrawingContextImpl.CreateInfo
+        {
+            Canvas = canvas,
+            Dpi = _dpi,
+            DisableTextLcdRendering = true,
+            GrContext = _grContext,
+            Gpu = _gpu,
+        };
+        return new DrawingContextImpl(createInfo, Disposable.Create(() =>
+        {
+            _picture = recorder.EndRecording();
+            canvas.Dispose();
+            recorder.Dispose();
+        }));
+    }
+
+    public void Dispose() => _picture?.Dispose();
+}

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

@@ -97,7 +97,7 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
             _canvas.RestoreToCount(-1);
             _canvas.ResetMatrix();
@@ -106,7 +106,6 @@ namespace Avalonia.Skia
             {
                 Surface = _surface.Surface,
                 Dpi = Dpi,
-                VisualBrushRenderer = visualBrushRenderer,
                 DisableTextLcdRendering = _disableLcdRendering,
                 GrContext = _grContext,
                 Gpu = _gpu,

+ 2 - 2
src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs

@@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1
             _externalRenderTargetProvider.DestroyRenderTarget();
         }
 
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
             var target =  _externalRenderTargetProvider.GetOrCreateRenderTarget();
             _externalRenderTargetProvider.BeforeDrawing();
-            return new DrawingContextImpl(visualBrushRenderer, null, target, null, () =>
+            return new DrawingContextImpl( null, target, null, () =>
             {
                 try
                 {

+ 4 - 4
src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1
         {            
         }
 
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
             var locked = _surface.Lock();
             if (locked.Format == PixelFormat.Rgb565)
@@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1
             }
 
             return new FramebufferShim(locked)
-                .CreateDrawingContext(visualBrushRenderer);
+                .CreateDrawingContext();
         }
 
         public bool IsCorrupted => false;
@@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1
                 _target = target;
             }
             
-            public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+            public override IDrawingContextImpl CreateDrawingContext()
             {
-                return base.CreateDrawingContext(visualBrushRenderer, () =>
+                return base.CreateDrawingContext(() =>
                 {
                     using (var l = WicImpl.Lock(BitmapLockFlags.Read))
                     {

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

@@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media
     /// </summary>
     internal class DrawingContextImpl : IDrawingContextImpl
     {
-        private readonly IVisualBrushRenderer _visualBrushRenderer;
         private readonly ILayerFactory _layerFactory;
         private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
         private readonly DeviceContext _deviceContext;
@@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="swapChain">An optional swap chain associated with this drawing context.</param>
         /// <param name="finishedCallback">An optional delegate to be called when context is disposed.</param>
         public DrawingContextImpl(
-            IVisualBrushRenderer visualBrushRenderer,
             ILayerFactory layerFactory,
             SharpDX.Direct2D1.RenderTarget renderTarget,
             SharpDX.DXGI.SwapChain1 swapChain = null,
             Action finishedCallback = null)
         {
-            _visualBrushRenderer = visualBrushRenderer;
             _layerFactory = layerFactory;
             _renderTarget = renderTarget;
             _swapChain = swapChain;
@@ -491,7 +488,8 @@ namespace Avalonia.Direct2D1.Media
             var radialGradientBrush = brush as IRadialGradientBrush;
             var conicGradientBrush = brush as IConicGradientBrush;
             var imageBrush = brush as IImageBrush;
-            var visualBrush = brush as IVisualBrush;
+            var sceneBrush = brush as ISceneBrush;
+            var sceneBrushContent = brush as ISceneBrushContent;
 
             if (solidColorBrush != null)
             {
@@ -518,11 +516,13 @@ namespace Avalonia.Direct2D1.Media
                     (BitmapImpl)imageBrush.Source.PlatformImpl.Item,
                     destinationSize);
             }
-            else if (visualBrush != null)
+            else if (sceneBrush != null || sceneBrushContent != null)
             {
-                if (_visualBrushRenderer != null)
+                sceneBrushContent ??= sceneBrush.CreateContent();
+                if (sceneBrushContent != null)
                 {
-                    var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+                    var rect = sceneBrushContent.Rect;
+                    var intermediateSize = rect.Size;
 
                     if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
                     {
@@ -533,28 +533,26 @@ namespace Avalonia.Direct2D1.Media
                         var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi);
 
                         using (var intermediate = new BitmapRenderTarget(
-                            _deviceContext,
-                            CompatibleRenderTargetOptions.None,
-                            pixelSize.ToSizeWithDpi(dpi).ToSharpDX()))
+                                   _deviceContext,
+                                   CompatibleRenderTargetOptions.None,
+                                   pixelSize.ToSizeWithDpi(dpi).ToSharpDX()))
                         {
-                            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer))
+                            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
                             {
                                 intermediate.Clear(null);
-                                _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
+                                sceneBrushContent.Render(ctx,
+                                    rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y));
                             }
 
                             return new ImageBrushImpl(
-                                visualBrush,
+                                sceneBrushContent.Brush,
                                 _deviceContext,
                                 new D2DBitmapImpl(intermediate.Bitmap),
                                 destinationSize);
                         }
+
                     }
                 }
-                else
-                {
-                    throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
-                }
             }
 
             return new SolidColorBrushImpl(null, _deviceContext);

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

@@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media
                 CompatibleRenderTargetOptions.None,
                 calc.IntermediateSize.ToSharpDX());
 
-            using (var context = new RenderTarget(result).CreateDrawingContext(null))
+            using (var context = new RenderTarget(result).CreateDrawingContext())
             {
                 var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height);
                 var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi));

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

@@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging
             return new D2DRenderTargetBitmapImpl(bitmapRenderTarget);
         }
 
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
-            return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++);
+            return new DrawingContextImpl( this, _renderTarget, null, () => Version++);
         }
 
         public bool IsCorrupted => false;

+ 4 - 4
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs

@@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media
             base.Dispose();
         }
 
-        public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
-            => CreateDrawingContext(visualBrushRenderer, null);
+        public virtual IDrawingContextImpl CreateDrawingContext()
+            => CreateDrawingContext(null);
 
         public bool IsCorrupted => false;
 
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback)
+        public IDrawingContextImpl CreateDrawingContext(Action finishedCallback)
         {
-            return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () =>
+            return new DrawingContextImpl(null, _renderTarget, finishedCallback: () =>
                 {
                     Version++;
                     finishedCallback?.Invoke();

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

@@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1
         /// Creates a drawing context for a rendering session.
         /// </summary>
         /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
-            return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget);
+            return new DrawingContextImpl(this, _renderTarget);
         }
 
         public bool IsCorrupted => false;

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

@@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1
         /// Creates a drawing context for a rendering session.
         /// </summary>
         /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        public IDrawingContextImpl CreateDrawingContext()
         {
             var size = GetWindowSize();
             var dpi = GetWindowDpi();
@@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1
                 Resize();
             }
 
-            return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain);
+            return new DrawingContextImpl(this, _deviceContext, _swapChain);
         }
 
         public bool IsCorrupted => false;

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

@@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests
 
         private DrawingContext CreateDrawingContext()
         {
-            return new DrawingContext(Mock.Of<IDrawingContextImpl>());
+            return new PlatformDrawingContext(Mock.Of<IDrawingContextImpl>());
         }
 
         private class TestControl : Control

+ 2 - 2
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
                 new Matrix(),
                 Brushes.Black,
                 null,
-                geometry, default);
+                geometry);
 
             geometryNode.HitTest(new Point());
         }
@@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
         private class TestRectangleDrawOperation : RectangleNode
         {
             public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) 
-                : base(transform, pen.Brush, pen, bounds, new BoxShadows())
+                : base(transform, pen.Brush?.ToImmutable(), pen, bounds, new BoxShadows())
             {
 
             }

+ 2 - 2
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         [InlineData(0, 101, false)]
         public void FillOnly_HitTest(double x, double y, bool inside)
         {
-            var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100), null);
+            var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100));
 
             var point = new Point(x, y);
 
@@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         [InlineData(0, 101, false)]
         public void StrokeOnly_HitTest(double x, double y, bool inside)
         {
-            var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100), null);
+            var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100));
 
             var point = new Point(x, y);
 

+ 1 - 1
tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Benchmarks.Rendering
             _lineFill = new Line { Fill = new SolidColorBrush() };
             _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() };
 
-            _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true);
+            _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true);
 
             AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new NullRenderingPlatform());
         }

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

@@ -76,7 +76,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             var r = Avalonia.AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
             using(var cpuContext = r.CreateBackendContext(null))
             using (var target = cpuContext.CreateRenderTarget(new object[] { fb }))
-            using (var ctx = target.CreateDrawingContext(null))
+            using (var ctx = target.CreateDrawingContext())
             {
                 ctx.Clear(Colors.Transparent);
                 ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80));
@@ -90,7 +90,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             fb.Deallocate();
             using (var rtb = new RenderTargetBitmap(new PixelSize(100, 100), new Vector(96, 96)))
             {
-                using (var ctx = rtb.CreateDrawingContext(null))
+                using (var ctx = rtb.CreateDrawingContext())
                 {
                     ctx.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 100, 100));
                     ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10));

+ 94 - 0
tests/Avalonia.RenderTests/Media/TileBrushTests.cs

@@ -0,0 +1,94 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests;
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media;
+#endif
+public class DrawingBrushTests: TestBase
+{
+    public DrawingBrushTests()
+        : base(@"Media\DrawingBrush")
+    {
+    }
+    
+    [Fact]
+    public async Task DrawingBrushIsProperlyTiled()
+    {
+        Decorator target = new Decorator
+        {
+            Padding = new Thickness(10),
+            Width = 220,
+            Height = 220,
+            Child = new Rectangle
+            {
+                Fill = new DrawingBrush
+                {
+                    Stretch = Stretch.None,
+                    TileMode = TileMode.Tile,
+                    Drawing = CreateDrawing(),
+                    DestinationRect = new RelativeRect(0,0,0.25,0.25, RelativeUnit.Relative)
+                }
+            }
+        };
+
+        await RenderToFile(target);
+        CompareImages();
+    }
+    
+
+#if AVALONIA_SKIA
+    [Fact]
+#endif
+    public async Task DrawingBrushIsProperlyUpscaled()
+    {
+        Decorator target = new Decorator
+        {
+            Padding = new Thickness(10),
+            Width = 420,
+            Height = 420,
+            Child = new Rectangle
+            {
+                Fill = new DrawingBrush
+                {
+                    Stretch = Stretch.Fill,
+                    TileMode = TileMode.None,
+                    Drawing = CreateDrawing()
+                }
+            }
+        };
+
+        await RenderToFile(target);
+        CompareImages();
+    }
+
+    GeometryDrawing CreateDrawing()
+    {
+        return new GeometryDrawing
+        {
+            Geometry = new GeometryGroup
+            {
+                Children =
+                {
+                    new RectangleGeometry(new Rect(50, 25, 25, 25)),
+                    new RectangleGeometry(new Rect(25, 50, 25, 25)),
+                }
+            },
+            Pen = new Pen(new LinearGradientBrush()
+            {
+                GradientStops =
+                {
+                    new GradientStop(Colors.Blue, 0),
+                    new GradientStop(Colors.Black, 1),
+                }
+            }, 5),
+            Brush = Brushes.Yellow,
+        };
+    }
+    
+}

+ 3 - 3
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -34,15 +34,15 @@ namespace Avalonia.UnitTests
                 
             }
 
-            public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+            public IDrawingContextImpl CreateDrawingContext()
             {
                 var m = new Mock<IDrawingContextImpl>();
                 m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
                     .Returns(() =>
                         {
                             var r = new Mock<IDrawingContextLayerImpl>();
-                            r.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()))
-                                .Returns(CreateDrawingContext(null));
+                            r.Setup(r => r.CreateDrawingContext())
+                                .Returns(CreateDrawingContext());
                             return r.Object;
                         }
                     );

+ 2 - 2
tests/Avalonia.UnitTests/TestRoot.cs

@@ -70,12 +70,12 @@ namespace Avalonia.UnitTests
             {
                 var layerDc = new Mock<IDrawingContextImpl>();
                 var layer = new Mock<IDrawingContextLayerImpl>();
-                layer.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(layerDc.Object);
+                layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object);
                 return layer.Object;
             });
 
             var result = new Mock<IRenderTarget>();
-            result.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(dc.Object);
+            result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object);
             return result.Object;
         }
 

BIN
tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png


BIN
tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png


BIN
tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png