瀏覽代碼

Skia backend cleanup.

Nelson Carrillo 7 年之前
父節點
當前提交
88bfdf87ea
共有 42 個文件被更改,包括 1831 次插入705 次删除
  1. 44 0
      Avalonia.sln
  2. 1 0
      build.cake
  3. 2 2
      build/SkiaSharp.props
  4. 2 2
      samples/ControlCatalog.NetCore/Program.cs
  5. 3 0
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  6. 1 0
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  7. 11 5
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  8. 51 1
      src/Avalonia.Visuals/Vector.cs
  9. 0 142
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  10. 515 268
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  11. 11 11
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  12. 164 47
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  13. 164 6
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  14. 47 0
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  15. 35 0
      src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
  16. 23 0
      src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
  17. 92 0
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  18. 46 34
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  19. 28 0
      src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs
  20. 15 31
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  21. 4 2
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  22. 69 55
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  23. 169 0
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  24. 23 39
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  25. 8 1
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  26. 151 0
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  27. 7 27
      src/Skia/Avalonia.Skia/readme.md
  28. 28 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  29. 1 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  30. 1 1
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  31. 2 10
      tests/Avalonia.RenderTests/Media/ImageBrushTests.cs
  32. 2 10
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  33. 1 5
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  34. 1 5
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  35. 22 0
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  36. 79 0
      tests/Avalonia.Skia.UnitTests/HitTesting.cs
  37. 8 0
      tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs
  38. 二進制
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png
  39. 二進制
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png
  40. 二進制
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png
  41. 二進制
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  42. 二進制
      tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png

+ 44 - 0
Avalonia.sln

@@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props
 		build\Moq.props = build\Moq.props
 		build\NetCore.props = build\NetCore.props
+		build\NetFX.props = build\NetFX.props
 		build\ReactiveUI.props = build\ReactiveUI.props
 		build\Rx.props = build\Rx.props
 		build\SampleApp.props = build\SampleApp.props
@@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -2468,6 +2471,46 @@ Global
 		{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
 		{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU
 		{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -2520,6 +2563,7 @@ Global
 		{F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2}
 		{4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+		{E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 1 - 0
build.cake

@@ -207,6 +207,7 @@ Task("Run-Unit-Tests")
         RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false);
         RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false);
         RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false);
+        RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false);
         if (data.Parameters.IsRunningOnWindows)
         {
             RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true);

+ 2 - 2
build/SkiaSharp.props

@@ -1,6 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SkiaSharp" Version="1.57.1" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.57.1.3" />
+    <PackageReference Include="SkiaSharp" Version="1.60.0" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.60.0.1" />
   </ItemGroup>
 </Project>

+ 2 - 2
samples/ControlCatalog.NetCore/Program.cs

@@ -1,9 +1,9 @@
 using System;
 using System.Diagnostics;
 using System.Linq;
-using System.Runtime.InteropServices;
 using System.Threading;
 using Avalonia;
+using Avalonia.Skia;
 
 namespace ControlCatalog.NetCore
 {
@@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore
         /// This method is needed for IDE previewer infrastructure
         /// </summary>
         public static AppBuilder BuildAvaloniaApp()
-            => AppBuilder.Configure<App>().UsePlatformDetect().UseReactiveUI();
+            => AppBuilder.Configure<App>().UsePlatformDetect().UseSkia().UseReactiveUI();
 
         static void ConsoleSilencer()
         {

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

@@ -117,6 +117,9 @@ namespace Avalonia.Rendering
             var scene = Interlocked.Exchange(ref _scene, null);
             scene?.Dispose();
             Stop();
+
+            Layers.Clear();
+            RenderTarget?.Dispose();
         }
 
         /// <inheritdoc/>

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

@@ -133,6 +133,7 @@ namespace Avalonia.Rendering
         /// </summary>
         public void Dispose()
         {
+            _renderTarget?.Dispose();
         }
 
         /// <inheritdoc/>

+ 11 - 5
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@@ -11,11 +11,7 @@ namespace Avalonia.Rendering
     {
         private List<RenderLayer> _inner = new List<RenderLayer>();
         private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
-
-        public RenderLayers()
-        {
-        }
-
+        
         public int Count => _inner.Count;
         public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
 
@@ -51,6 +47,16 @@ namespace Avalonia.Rendering
             }
         }
 
+        public void Clear()
+        {
+            foreach (var layer in _index.Values)
+            {
+                layer.Bitmap.Dispose();
+            }
+
+            _index.Clear();
+        }
+
         public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
         {
             return _index.TryGetValue(layerRoot, out value);

+ 51 - 1
src/Avalonia.Visuals/Vector.cs

@@ -3,7 +3,7 @@
 
 using System;
 using System.Globalization;
-using System.Xml.Linq;
+using JetBrains.Annotations;
 
 namespace Avalonia
 {
@@ -122,6 +122,56 @@ namespace Avalonia
             return new Vector(a._x - b._x, a._y - b._y);
         }
 
+        /// <summary>
+        /// Check if two vectors are equal (bitwise).
+        /// </summary>
+        /// <param name="other"></param>
+        /// <returns></returns>
+        public bool Equals(Vector other)
+        {
+            // ReSharper disable CompareOfFloatsByEqualityOperator
+            return _x == other._x && _y == other._y;
+            // ReSharper restore CompareOfFloatsByEqualityOperator
+        }
+
+        /// <summary>
+        /// Check if two vectors are nearly equal (numerically).
+        /// </summary>
+        /// <param name="other">The other vector.</param>
+        /// <returns>True if vectors are nearly equal.</returns>
+        [Pure]
+        public bool NearlyEquals(Vector other)
+        {
+            const float tolerance = float.Epsilon;
+
+            return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
+        }
+
+        public override bool Equals(object obj)
+        {
+            if (ReferenceEquals(null, obj)) return false;
+
+            return obj is Vector vector && Equals(vector);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                return (_x.GetHashCode() * 397) ^ _y.GetHashCode();
+            }
+        }
+
+        public static bool operator ==(Vector left, Vector right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(Vector left, Vector right)
+        {
+            return !left.Equals(right);
+        }
+
         /// <summary>
         /// Returns the string representation of the point.
         /// </summary>

+ 0 - 142
src/Skia/Avalonia.Skia/BitmapImpl.cs

@@ -1,142 +0,0 @@
-using System;
-using System.IO;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
-    class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl
-    {
-        private Vector _dpi;
-
-        public SKBitmap Bitmap { get; private set; }
-
-        public BitmapImpl(SKBitmap bm)
-        {
-            Bitmap = bm;
-            PixelHeight = bm.Height;
-            PixelWidth = bm.Width;
-            _dpi = new Vector(96, 96);
-        }
-
-        static void ReleaseProc(IntPtr address, object ctx)
-        {
-            ((IUnmanagedBlob) ctx).Dispose();
-        }
-
-        private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc;
-        
-        public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null)
-        {
-            PixelHeight = height;
-            PixelWidth = width;
-            _dpi = dpi;
-            var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
-            var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
-            var runtime = runtimePlatform?.GetRuntimeInfo();
-            if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux)
-                colorType = SKColorType.Bgra8888;
-
-            if (runtimePlatform != null)
-            {
-                Bitmap = new SKBitmap();
-                var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
-                var plat = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
-                var blob = plat.AllocBlob(nfo.BytesSize);
-                Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob);
-                
-            }
-            else 
-                Bitmap =  new SKBitmap(width, height, colorType, SKAlphaType.Premul);
-            Bitmap.Erase(SKColor.Empty);
-        }
-
-        public void Dispose()
-        {
-            Bitmap.Dispose();
-        }
-
-        public int PixelWidth { get; private set; }
-        public int PixelHeight { get; private set; }
-
-        class BitmapDrawingContext : DrawingContextImpl
-        {
-            private readonly SKSurface _surface;
-
-            public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
-                : this(CreateSurface(bitmap), dpi, visualBrushRenderer)
-            {
-                CanUseLcdRendering = false;
-            }
-
-            private static SKSurface CreateSurface(SKBitmap bitmap)
-            {
-                IntPtr length;
-                var rv =  SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes);
-                if (rv == null)
-                    throw new Exception("Unable to create Skia surface");
-                return rv;
-            }
-
-            public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
-                : base(surface.Canvas, dpi, visualBrushRenderer)
-            {
-                _surface = surface;
-            }
-
-            public override void Dispose()
-            {
-                base.Dispose();
-                _surface.Dispose();
-            }
-        }
-
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
-        {
-            return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer);
-        }
-
-        public void Save(Stream stream)
-        {
-            IntPtr length;
-            using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes))
-            using (var data = image.Encode())
-            {
-                data.SaveTo(stream);
-            }
-        }
-
-        public void Save(string fileName)
-        {
-            using (var stream = File.Create(fileName))
-                Save(stream);
-        }
-
-        class BitmapFramebuffer : ILockedFramebuffer
-        {
-            private SKBitmap _bmp;
-
-            public BitmapFramebuffer(SKBitmap bmp)
-            {
-                _bmp = bmp;
-                _bmp.LockPixels();
-            }
-
-            public void Dispose()
-            {
-                _bmp.UnlockPixels();
-                _bmp = null;
-            }
-
-            public IntPtr Address => _bmp.GetPixels();
-            public int Width => _bmp.Width;
-            public int Height => _bmp.Height;
-            public int RowBytes => _bmp.RowBytes;
-            public Vector Dpi { get; } = new Vector(96, 96);
-            public PixelFormat Format => _bmp.ColorType.ToPixelFormat();
-        }
-
-        public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap);
-    }
-}

+ 515 - 268
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -1,57 +1,114 @@
-using Avalonia.Media;
-using SkiaSharp;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Utilities;
 using Avalonia.Utilities;
+using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    internal class DrawingContextImpl : IDrawingContextImpl
+    /// <summary>
+    /// Skia based drawing context.
+    /// </summary>
+    public class DrawingContextImpl : IDrawingContextImpl
     {
+        private readonly IDisposable[] _disposables;
         private readonly Vector _dpi;
+        private readonly Stack<PaintWrapper> _maskStack = new Stack<PaintWrapper>();
+        private readonly Stack<double> _opacityStack = new Stack<double>();
         private readonly Matrix? _postTransform;
-        private readonly IDisposable[] _disposables;
         private readonly IVisualBrushRenderer _visualBrushRenderer;
-        private Stack<PaintWrapper> maskStack = new Stack<PaintWrapper>();
-        protected bool CanUseLcdRendering = true;
-        public SKCanvas Canvas { get; private set; }
-
-        public DrawingContextImpl(
-            SKCanvas canvas,
-            Vector dpi,
-            IVisualBrushRenderer visualBrushRenderer,
-            params IDisposable[] disposables)
+        private double _currentOpacity = 1.0f;
+        private readonly bool _canTextUseLcdRendering;
+        private Matrix _currentTransform;
+
+        /// <summary>
+        /// Context create info.
+        /// </summary>
+        public struct CreateInfo
+        {
+            /// <summary>
+            /// Canvas to draw to.
+            /// </summary>
+            public SKCanvas Canvas;
+
+            /// <summary>
+            /// Dpi of drawings.
+            /// </summary>
+            public Vector Dpi;
+
+            /// <summary>
+            /// Visual brush renderer.
+            /// </summary>
+            public IVisualBrushRenderer VisualBrushRenderer;
+
+            /// <summary>
+            /// Render text without Lcd rendering.
+            /// </summary>
+            public bool DisableTextLcdRendering;
+        }
+
+        /// <summary>
+        /// Create new drawing context.
+        /// </summary>
+        /// <param name="createInfo">Create info.</param>
+        /// <param name="disposables">Array of elements to dispose after drawing has finished.</param>
+        public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables)
         {
-            _dpi = dpi;
-            if (dpi.X != 96 || dpi.Y != 96)
-                _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
-            _visualBrushRenderer = visualBrushRenderer;
+            _dpi = createInfo.Dpi;
+            _visualBrushRenderer = createInfo.VisualBrushRenderer;
             _disposables = disposables;
-            Canvas = canvas;
+            _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering;
+
+            Canvas = createInfo.Canvas;
+
+            if (Canvas == null)
+            {
+                throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
+            }
+
+            if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
+            {
+                _postTransform =
+                    Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+            }
+
             Transform = Matrix.Identity;
         }
+        
+        /// <summary>
+        /// Skia canvas.
+        /// </summary>
+        public SKCanvas Canvas { get; }
 
+        /// <inheritdoc />
         public void Clear(Color color)
         {
             Canvas.Clear(color.ToSKColor());
         }
 
+        /// <inheritdoc />
         public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
         {
-            var impl = (BitmapImpl)source.Item;
+            var drawableImage = (IDrawableBitmapImpl) source.Item;
             var s = sourceRect.ToSKRect();
             var d = destRect.ToSKRect();
-            using (var paint = new SKPaint()
-                    { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) })
+
+            using (var paint =
+                new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))})
             {
-                Canvas.DrawBitmap(impl.Bitmap, s, d, paint);
+                drawableImage.Draw(this, s, d, paint);
             }
         }
 
+        /// <inheritdoc />
         public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
         {
             PushOpacityMask(opacityMask, opacityMaskRect);
@@ -59,17 +116,19 @@ namespace Avalonia.Skia
             PopOpacityMask();
         }
 
+        /// <inheritdoc />
         public void DrawLine(Pen pen, Point p1, Point p2)
         {
             using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
             {
-                Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint);
+                Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
             }
         }
 
+        /// <inheritdoc />
         public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
         {
-            var impl = (GeometryImpl)geometry;
+            var impl = (GeometryImpl) geometry;
             var size = geometry.Bounds.Size;
 
             using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
@@ -79,6 +138,7 @@ namespace Avalonia.Skia
                 {
                     Canvas.DrawPath(impl.EffectivePath, fill.Paint);
                 }
+
                 if (stroke.Paint != null)
                 {
                     Canvas.DrawPath(impl.EffectivePath, stroke.Paint);
@@ -86,227 +146,424 @@ namespace Avalonia.Skia
             }
         }
 
-        private struct PaintState : IDisposable
+        /// <inheritdoc />
+        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
         {
-            private readonly SKColor _color;
-            private readonly SKShader _shader;
-            private readonly SKPaint _paint;
+            using (var paint = CreatePaint(pen, rect.Size))
+            {
+                var rc = rect.ToSKRect();
 
-            public PaintState(SKPaint paint, SKColor color, SKShader shader)
+                if (Math.Abs(cornerRadius) < float.Epsilon)
+                {
+                    Canvas.DrawRect(rc, paint.Paint);
+                }
+                else
+                {
+                    Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+        {
+            using (var paint = CreatePaint(brush, rect.Size))
             {
-                _paint = paint;
-                _color = color;
-                _shader = shader;
+                var rc = rect.ToSKRect();
+
+                if (Math.Abs(cornerRadius) < float.Epsilon)
+                {
+                    Canvas.DrawRect(rc, paint.Paint);
+                }
+                else
+                {
+                    Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
+                }
             }
+        }
 
-            public void Dispose()
+        /// <inheritdoc />
+        public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+        {
+            using (var paint = CreatePaint(foreground, text.Size))
             {
-                _paint.Color = _color;
-                _paint.Shader = _shader;
+                var textImpl = (FormattedTextImpl) text;
+                textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
             }
         }
 
-        internal struct PaintWrapper : IDisposable
+        /// <inheritdoc />
+        public IRenderTargetBitmapImpl CreateLayer(Size size)
         {
-            //We are saving memory allocations there
-            //TODO: add more disposable fields if needed
-            public readonly SKPaint Paint;
+            var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+            var pixelSize = size * normalizedDpi;
 
-            private IDisposable _disposable1;
-            private IDisposable _disposable2;
+            return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi);
+        }
 
-            public IDisposable ApplyTo(SKPaint paint)
-            {
-                var state = new PaintState(paint, paint.Color, paint.Shader);
+        /// <inheritdoc />
+        public void PushClip(Rect clip)
+        {
+            Canvas.Save();
+            Canvas.ClipRect(clip.ToSKRect());
+        }
 
-                paint.Color = Paint.Color;
-                paint.Shader = Paint.Shader;
+        /// <inheritdoc />
+        public void PopClip()
+        {
+            Canvas.Restore();
+        }
 
-                return state;
-            }
+        /// <inheritdoc />
+        public void PushOpacity(double opacity)
+        {
+            _opacityStack.Push(_currentOpacity);
+            _currentOpacity *= opacity;
+        }
 
-            public void AddDisposable(IDisposable disposable)
-            {
-                if (_disposable1 == null)
-                    _disposable1 = disposable;
-                else if (_disposable2 == null)
-                    _disposable2 = disposable;
-                else
-                    throw new InvalidOperationException();
-            }
+        /// <inheritdoc />
+        public void PopOpacity()
+        {
+            _currentOpacity = _opacityStack.Pop();
+        }
 
-            public PaintWrapper(SKPaint paint)
+        /// <inheritdoc />
+        public virtual void Dispose()
+        {
+            if (_disposables == null)
             {
-                Paint = paint;
-                _disposable1 = null;
-                _disposable2 = null;
+                return;
             }
 
-            public void Dispose()
+            foreach (var disposable in _disposables)
             {
-                Paint?.Dispose();
-                _disposable1?.Dispose();
-                _disposable2?.Dispose();
+                disposable?.Dispose();
             }
         }
 
-        internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+        /// <inheritdoc />
+        public void PushGeometryClip(IGeometryImpl clip)
         {
-            SKPaint paint = new SKPaint();
-            var rv = new PaintWrapper(paint);
-            paint.IsStroke = false;
+            Canvas.Save();
+            Canvas.ClipPath(((GeometryImpl)clip).EffectivePath);
+        }
 
-            
-            double opacity = brush.Opacity * _currentOpacity;
-            paint.IsAntialias = true;
+        /// <inheritdoc />
+        public void PopGeometryClip()
+        {
+            Canvas.Restore();
+        }
+
+        /// <inheritdoc />
+        public void PushOpacityMask(IBrush mask, Rect bounds)
+        {
+            // TODO: This should be disposed
+            var paint = new SKPaint();
+
+            Canvas.SaveLayer(paint);
+            _maskStack.Push(CreatePaint(mask, bounds.Size));
+        }
 
-            var solid = brush as ISolidColorBrush;
-            if (solid != null)
+        /// <inheritdoc />
+        public void PopOpacityMask()
+        {
+            using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
             {
-                paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
-                return rv;
+                Canvas.SaveLayer(paint);
+                using (var paintWrapper = _maskStack.Pop())
+                {
+                    Canvas.DrawPaint(paintWrapper.Paint);
+                }
+                Canvas.Restore();
             }
-            paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity)));
 
-            var gradient = brush as IGradientBrush;
-            if (gradient != null)
+            Canvas.Restore();
+        }
+
+        /// <inheritdoc />
+        public Matrix Transform
+        {
+            get { return _currentTransform; }
+            set
             {
-                var tileMode = gradient.SpreadMethod.ToSKShaderTileMode();
-                var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
-                var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray();
+                if (_currentTransform == value)
+                    return;
+
+                _currentTransform = value;
+
+                var transform = value;
 
-                var linearGradient = brush as ILinearGradientBrush;
-                if (linearGradient != null)
+                if (_postTransform.HasValue)
+                {
+                    transform *= _postTransform.Value;
+                }
+
+                Canvas.SetMatrix(transform.ToSKMatrix());
+            }
+        }
+
+        /// <summary>
+        /// Configure paint wrapper for using gradient brush.
+        /// </summary>
+        /// <param name="paintWrapper">Paint wrapper.</param>
+        /// <param name="targetSize">Target size.</param>
+        /// <param name="gradientBrush">Gradient brush.</param>
+        private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
+        {
+            var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
+            var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
+            var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
+
+            switch (gradientBrush)
+            {
+                case ILinearGradientBrush linearGradient:
                 {
                     var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
                     var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
 
                     // would be nice to cache these shaders possibly?
-                    using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
-                        paint.Shader = shader;
+                    using (var shader =
+                        SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
+                    {
+                        paintWrapper.Paint.Shader = shader;
+                    }
 
+                    break;
                 }
-                else
+                case IRadialGradientBrush radialGradient:
                 {
-                    var radialGradient = brush as IRadialGradientBrush;
-                    if (radialGradient != null)
-                    {
-                        var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
-                        var radius = (float)radialGradient.Radius;
+                    var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
+                    var radius = (float)(radialGradient.Radius * targetSize.Width);
 
-                        // TODO: There is no SetAlpha in SkiaSharp
-                        //paint.setAlpha(128);
-
-                        // would be nice to cache these shaders possibly?
-                        using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
-                            paint.Shader = shader;
+                    // TODO: There is no SetAlpha in SkiaSharp
+                    //paint.setAlpha(128);
 
+                    // would be nice to cache these shaders possibly?
+                    using (var shader =
+                        SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
+                    {
+                        paintWrapper.Paint.Shader = shader;
                     }
+
+                    break;
                 }
+            }
+        }
+
+        /// <summary>
+        /// Configure paint wrapper for using tile brush.
+        /// </summary>
+        /// <param name="paintWrapper">Paint wrapper.</param>
+        /// <param name="targetSize">Target size.</param>
+        /// <param name="tileBrush">Tile brush to use.</param>
+        /// <param name="tileBrushImage">Tile brush image.</param>
+        private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
+        {
+            var calc = new TileBrushCalculator(tileBrush,
+                    new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
+
+            var intermediate = CreateRenderTarget(
+                (int)calc.IntermediateSize.Width,
+                (int)calc.IntermediateSize.Height, _dpi);
+
+            paintWrapper.AddDisposable(intermediate);
+
+            using (var context = intermediate.CreateDrawingContext(null))
+            {
+                var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
 
-                return rv;
+                context.Clear(Colors.Transparent);
+                context.PushClip(calc.IntermediateClip);
+                context.Transform = calc.IntermediateTransform;
+                context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
+                context.PopClip();
             }
 
-            var tileBrush = brush as ITileBrush;
-            var visualBrush = brush as IVisualBrush;
-            var tileBrushImage = default(BitmapImpl);
+            var tileTransform =
+                tileBrush.TileMode != TileMode.None
+                    ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
+                    : SKMatrix.MakeIdentity();
 
-            if (visualBrush != null)
+            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;
+
+
+            var image = intermediate.SnapshotImage();
+            paintWrapper.AddDisposable(image);
+
+            using (var shader = image.ToShader(tileX, tileY, tileTransform))
             {
-                if (_visualBrushRenderer != null)
-                {
-                    var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+                paintWrapper.Paint.Shader = shader;
+            }
+        }
 
-                    if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
-                    {
-                        var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+        /// <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)
+        {
+            if (_visualBrushRenderer == null)
+            {
+                throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+            }
 
-                        using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
-                        {
-                            ctx.Clear(Colors.Transparent);
-                            _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
-                        }
+            var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
 
-                        tileBrushImage = intermediate;
-                        rv.AddDisposable(tileBrushImage);
-                    }
-                }
-                else
+            if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
+            {
+                var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+
+                using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer))
                 {
-                    throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+                    ctx.Clear(Colors.Transparent);
+
+                    visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
                 }
+
+                tileBrushImage = intermediate;
+                paintWrapper.AddDisposable(tileBrushImage);
             }
-            else
+        }
+
+        /// <summary>
+        /// Creates paint wrapper for given brush.
+        /// </summary>
+        /// <param name="brush">Source brush.</param>
+        /// <param name="targetSize">Target size.</param>
+        /// <returns>Paint wrapper for given brush.</returns>
+        internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+        {
+            var paint = new SKPaint
+            {
+                IsStroke = false,
+                IsAntialias = true
+            };
+
+            var paintWrapper = new PaintWrapper(paint);
+
+            double opacity = brush.Opacity * _currentOpacity;
+
+            if (brush is ISolidColorBrush solid)
             {
-                tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item);
+                paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
+
+                return paintWrapper;
             }
 
-            if (tileBrush != null && tileBrushImage != null)
+            paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity));
+
+            if (brush is IGradientBrush gradient)
             {
-                var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
-                var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi);
-                rv.AddDisposable(bitmap);
-                using (var context = bitmap.CreateDrawingContext(null))
-                {
-                    var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
+                ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
 
-                    context.Clear(Colors.Transparent);
-                    context.PushClip(calc.IntermediateClip);
-                    context.Transform = calc.IntermediateTransform;
-                    context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
-                    context.PopClip();
-                }
+                return paintWrapper;
+            }
+
+            var tileBrush = brush as ITileBrush;
+            var visualBrush = brush as IVisualBrush;
+            var tileBrushImage = default(IDrawableBitmapImpl);
 
-                SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y);
-                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;
-                using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation))
-                    paint.Shader = shader;
+            if (visualBrush != null)
+            {
+                ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage);
+            }
+            else
+            {
+                tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
             }
 
-            return rv;
+            if (tileBrush != null && tileBrushImage != null)
+            {
+                ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
+            }
+
+            return paintWrapper;
         }
 
+        /// <summary>
+        /// Creates paint wrapper for given pen.
+        /// </summary>
+        /// <param name="pen">Source pen.</param>
+        /// <param name="targetSize">Target size.</param>
+        /// <returns></returns>
         private PaintWrapper CreatePaint(Pen pen, Size targetSize)
         {
             var rv = CreatePaint(pen.Brush, targetSize);
             var paint = rv.Paint;
 
             paint.IsStroke = true;
-            paint.StrokeWidth = (float)pen.Thickness;
+            paint.StrokeWidth = (float) pen.Thickness;
 
-            if (pen.StartLineCap == PenLineCap.Round)
-                paint.StrokeCap = SKStrokeCap.Round;
-            else if (pen.StartLineCap == PenLineCap.Square)
-                paint.StrokeCap = SKStrokeCap.Square;
-            else
-                paint.StrokeCap = SKStrokeCap.Butt;
+            // Need to modify dashes due to Skia modifying their lengths
+            // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
+            // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
+            float dashLengthModifier;
+            float gapLengthModifier;
 
-            if (pen.LineJoin == PenLineJoin.Miter)
-                paint.StrokeJoin = SKStrokeJoin.Miter;
-            else if (pen.LineJoin == PenLineJoin.Round)
-                paint.StrokeJoin = SKStrokeJoin.Round;
-            else
-                paint.StrokeJoin = SKStrokeJoin.Bevel;
+            switch (pen.StartLineCap)
+            {
+                case PenLineCap.Round:
+                    paint.StrokeCap = SKStrokeCap.Round;
+                    dashLengthModifier = -paint.StrokeWidth;
+                    gapLengthModifier = paint.StrokeWidth;
+                    break;
+                case PenLineCap.Square:
+                    paint.StrokeCap = SKStrokeCap.Square;
+                    dashLengthModifier = -paint.StrokeWidth;
+                    gapLengthModifier = paint.StrokeWidth;
+                    break;
+                default:
+                    paint.StrokeCap = SKStrokeCap.Butt;
+                    dashLengthModifier = 0.0f;
+                    gapLengthModifier = 0.0f;
+                    break;
+            }
+
+            switch (pen.LineJoin)
+            {
+                case PenLineJoin.Miter:
+                    paint.StrokeJoin = SKStrokeJoin.Miter;
+                    break;
+                case PenLineJoin.Round:
+                    paint.StrokeJoin = SKStrokeJoin.Round;
+                    break;
+                default:
+                    paint.StrokeJoin = SKStrokeJoin.Bevel;
+                    break;
+            }
 
-            paint.StrokeMiter = (float)pen.MiterLimit;
+            paint.StrokeMiter = (float) pen.MiterLimit;
 
             if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
             {
-                var pe = SKPathEffect.CreateDash(
-                    pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(),
-                    (float)pen.DashStyle.Offset);
+                var srcDashes = pen.DashStyle.Dashes;
+                var dashesArray = new float[srcDashes.Count];
+
+                for (var i = 0; i < srcDashes.Count; ++i)
+                {
+                    var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier;
+
+                    // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale
+                    dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier;
+                }
+
+                var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset);
+
                 paint.PathEffect = pe;
                 rv.AddDisposable(pe);
             }
@@ -314,128 +571,118 @@ namespace Avalonia.Skia
             return rv;
         }
 
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+        /// <summary>
+        /// Create new render target compatible with this drawing context.
+        /// </summary>
+        /// <param name="width">Width.</param>
+        /// <param name="height">Height.</param>
+        /// <param name="dpi">Drawing dpi.</param>
+        /// <param name="format">Pixel format.</param>
+        /// <returns></returns>
+        private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null)
         {
-            using (var paint = CreatePaint(pen, rect.Size))
+            var createInfo = new SurfaceRenderTarget.CreateInfo
             {
-                var rc = rect.ToSKRect();
-                if (cornerRadius == 0)
-                {
-                    Canvas.DrawRect(rc, paint.Paint);
-                }
-                else
-                {
-                    Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
-                }
-            }
+                Width = width,
+                Height = height,
+                Dpi = dpi,
+                Format = format,
+                DisableTextLcdRendering = !_canTextUseLcdRendering
+            };
+
+            return new SurfaceRenderTarget(createInfo);
         }
 
-        public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+        /// <summary>
+        /// Skia cached paint state.
+        /// </summary>
+        private struct PaintState : IDisposable
         {
-            using (var paint = CreatePaint(brush, rect.Size))
+            private readonly SKColor _color;
+            private readonly SKShader _shader;
+            private readonly SKPaint _paint;
+            
+            public PaintState(SKPaint paint, SKColor color, SKShader shader)
             {
-                var rc = rect.ToSKRect();
-                if (cornerRadius == 0)
-                {
-                    Canvas.DrawRect(rc, paint.Paint);
-                }
-                else
-                {
-                    Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
-                }
+                _paint = paint;
+                _color = color;
+                _shader = shader;
             }
-        }
 
-        public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
-        {
-            using (var paint = CreatePaint(foreground, text.Size))
+            /// <inheritdoc />
+            public void Dispose()
             {
-                var textImpl = (FormattedTextImpl)text;
-                textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering);
+                _paint.Color = _color;
+                _paint.Shader = _shader;
             }
         }
 
-        public IRenderTargetBitmapImpl CreateLayer(Size size)
-        {
-            var pixelSize = size * (_dpi / 96);
-            return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi);
-        }
-
-        public void PushClip(Rect clip)
-        {
-            Canvas.Save();
-            Canvas.ClipRect(clip.ToSKRect());
-        }
-
-        public void PopClip()
-        {
-            Canvas.Restore();
-        }
-
-        private double _currentOpacity = 1.0f;
-        private readonly Stack<double> _opacityStack = new Stack<double>();
-
-        public void PushOpacity(double opacity)
+        /// <summary>
+        /// Skia paint wrapper.
+        /// </summary>
+        internal struct PaintWrapper : IDisposable
         {
-            _opacityStack.Push(_currentOpacity);
-            _currentOpacity *= opacity;
-        }
+            //We are saving memory allocations there
+            public readonly SKPaint Paint;
 
-        public void PopOpacity()
-        {
-            _currentOpacity = _opacityStack.Pop();
-        }
+            private IDisposable _disposable1;
+            private IDisposable _disposable2;
+            private IDisposable _disposable3;
 
-        public virtual void Dispose()
-        {
-            if(_disposables!=null)
-                foreach (var disposable in _disposables)
-                    disposable?.Dispose();
-        }
+            public PaintWrapper(SKPaint paint)
+            {
+                Paint = paint;
 
-        public void PushGeometryClip(IGeometryImpl clip)
-        {
-            Canvas.Save();
-            Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath);
-        }
+                _disposable1 = null;
+                _disposable2 = null;
+                _disposable3 = null;
+            }
 
-        public void PopGeometryClip()
-        {
-            Canvas.Restore();
-        }
+            public IDisposable ApplyTo(SKPaint paint)
+            {
+                var state = new PaintState(paint, paint.Color, paint.Shader);
 
-        public void PushOpacityMask(IBrush mask, Rect bounds)
-        {
-            Canvas.SaveLayer(new SKPaint());
-            maskStack.Push(CreatePaint(mask, bounds.Size));
-        }
+                paint.Color = Paint.Color;
+                paint.Shader = Paint.Shader;
 
-        public void PopOpacityMask()
-        {
-            Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn });
-            using (var paintWrapper = maskStack.Pop())
-            {
-                Canvas.DrawPaint(paintWrapper.Paint);
+                return state;
             }
-            Canvas.Restore();
-            Canvas.Restore();
-        }
 
-        private Matrix _currentTransform;
-
-        public Matrix Transform
-        {
-            get { return _currentTransform; }
-            set
+            /// <summary>
+            /// Add new disposable to a wrapper.
+            /// </summary>
+            /// <param name="disposable">Disposable to add.</param>
+            public void AddDisposable(IDisposable disposable)
             {
-                if (_currentTransform == value)
-                    return;
+                if (_disposable1 == null)
+                {
+                    _disposable1 = disposable;
+                }
+                else if (_disposable2 == null)
+                {
+                    _disposable2 = disposable;
+                }
+                else if (_disposable3 == null)
+                {
+                    _disposable3 = disposable;
+                }
+                else
+                {
+                    Debug.Assert(false);
 
-                _currentTransform = value;
-                var transform = value;
-                if (_postTransform.HasValue)
-                    transform *= _postTransform.Value;
-                Canvas.SetMatrix(transform.ToSKMatrix());
+                    // ReSharper disable once HeuristicUnreachableCode
+                    throw new InvalidOperationException(
+                        "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables.");
+                }
+            }
+            
+            /// <inheritdoc />
+            public void Dispose()
+            {
+                Paint?.Dispose();
+                _disposable1?.Dispose();
+                _disposable2?.Dispose();
+                _disposable3?.Dispose();
             }
         }
     }

+ 11 - 11
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -10,6 +10,9 @@ using System.Linq;
 
 namespace Avalonia.Skia
 {
+    /// <summary>
+    /// Skia formatted text implementation.
+    /// </summary>
     public class FormattedTextImpl : IFormattedTextImpl
     {
         public FormattedTextImpl(
@@ -21,7 +24,7 @@ namespace Avalonia.Skia
             IReadOnlyList<FormattedTextStyleSpan> spans)
         {
             Text = text ?? string.Empty;
-
+            
             // Replace 0 characters with zero-width spaces (200B)
             Text = Text.Replace((char)0, (char)0x200B);
 
@@ -352,7 +355,7 @@ namespace Avalonia.Skia
             {
                 float measuredWidth;
                 string subText = textInput.Substring(textIndex, stop - textIndex);
-                lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2;
+                lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth);
             }
 
             //Check for white space or line breakers before the lengthBreak
@@ -451,7 +454,6 @@ namespace Avalonia.Skia
         private void BuildRects()
         {
             // Build character rects
-            var fm = _paint.FontMetrics;
             SKTextAlign align = _paint.TextAlign;
 
             for (int li = 0; li < _skiaLines.Count; li++)
@@ -559,18 +561,16 @@ namespace Avalonia.Skia
 
             string subString;
 
-            float widthConstraint = (_constraint.Width != double.PositiveInfinity)
-                                        ? (float)_constraint.Width
-                                        : -1;
-
-            for (int c = 0; curOff < length; c++)
+            float widthConstraint = double.IsPositiveInfinity(_constraint.Width)
+                                        ? -1
+                                        : (float)_constraint.Width;
+            
+            while(curOff < length)
             {
                 float lineWidth = -1;
                 int measured;
                 int trailingnumber = 0;
-
-                subString = Text.Substring(curOff);
-
+                
                 float constraint = -1;
 
                 if (_wrapping == TextWrapping.Wrap)

+ 164 - 47
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -1,82 +1,199 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Disposables;
 using Avalonia.Controls.Platform.Surfaces;
-using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Skia.Helpers;
 using SkiaSharp;
 
 namespace Avalonia.Skia
 {
+    /// <summary>
+    /// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
+    /// </summary>
     public class FramebufferRenderTarget : IRenderTarget
     {
-        private readonly IFramebufferPlatformSurface _surface;
+        private readonly IFramebufferPlatformSurface _platformSurface;
+        private SKImageInfo _currentImageInfo;
+        private IntPtr _currentFramebufferAddress;
+        private SKSurface _framebufferSurface;
+        private PixelFormatConversionShim _conversionShim;
+        private IDisposable _preFramebufferCopyHandler;
 
-        public FramebufferRenderTarget(IFramebufferPlatformSurface surface)
+        /// <summary>
+        /// Create new framebuffer render target using a target surface.
+        /// </summary>
+        /// <param name="platformSurface">Target surface.</param>
+        public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface)
         {
-            _surface = surface;
+            _platformSurface = platformSurface;
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            //Nothing to do here, since we don't own framebuffer
+            FreeSurface();
+        }
+
+        /// <inheritdoc />
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        {
+            var framebuffer = _platformSurface.Lock();
+            var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height,
+                framebuffer.Format.ToSkColorType(), SKAlphaType.Premul);
+
+            CreateSurface(framebufferImageInfo, framebuffer);
+
+            var canvas = _framebufferSurface.Canvas;
+
+            canvas.RestoreToCount(-1);
+            canvas.Save();
+            canvas.ResetMatrix();
+
+            var createInfo = new DrawingContextImpl.CreateInfo
+            {
+                Canvas = canvas,
+                Dpi = framebuffer.Dpi,
+                VisualBrushRenderer = visualBrushRenderer,
+                DisableTextLcdRendering = true
+            };
+
+            return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer);
+        }
+
+        /// <summary>
+        /// Check if two images info are compatible.
+        /// </summary>
+        /// <param name="currentImageInfo">Current.</param>
+        /// <param name="desiredImageInfo">Desired.</param>
+        /// <returns>True, if images are compatible.</returns>
+        private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo)
+        {
+            return currentImageInfo.Width == desiredImageInfo.Width &&
+                   currentImageInfo.Height == desiredImageInfo.Height &&
+                   currentImageInfo.ColorType == desiredImageInfo.ColorType;
+        }
+
+        /// <summary>
+        /// Create Skia surface backed by given framebuffer.
+        /// </summary>
+        /// <param name="desiredImageInfo">Desired image info.</param>
+        /// <param name="framebuffer">Backing framebuffer.</param>
+        private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer)
+        {
+            if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address)
+            {
+                return;
+            }
+            
+            FreeSurface();
+            
+            _currentFramebufferAddress = framebuffer.Address;
+
+            var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes);
+
+            // If surface cannot be created - try to create a compatibilty shim first
+            if (surface == null)
+            {
+                _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address);
+                _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler;
+
+                surface = _conversionShim.Surface;
+            }
+
+            _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " +
+                                                                 framebuffer.Format +
+                                                                 " or pixel format translator");
+            _currentImageInfo = desiredImageInfo;
+        }
+
+        /// <summary>
+        /// Free Skia surface.
+        /// </summary>
+        private void FreeSurface()
+        {
+            _conversionShim?.Dispose();
+            _conversionShim = null;
+            _preFramebufferCopyHandler = null;
+
+            if (_conversionShim != null)
+            {
+                _framebufferSurface?.Dispose();
+            }
+
+            _framebufferSurface = null;
+            _currentFramebufferAddress = IntPtr.Zero;
         }
 
-        class PixelFormatShim : IDisposable
+        /// <summary>
+        /// Converts non-compatible pixel formats using bitmap copies.
+        /// </summary>
+        private class PixelFormatConversionShim : IDisposable
         {
-            private readonly SKImageInfo _nfo;
-            private readonly IntPtr _fb;
-            private readonly int _rowBytes;
-            private SKBitmap _bitmap;
+            private readonly SKBitmap _bitmap;
+            private readonly SKImageInfo _destinationInfo;
+            private readonly IntPtr _framebufferAddress;
 
-            public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes)
+            public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress)
             {
-                _nfo = nfo;
-                _fb = fb;
-                _rowBytes = rowBytes;
+                _destinationInfo = destinationInfo;
+                _framebufferAddress = framebufferAddress;
+
+                // Create bitmap using default platform settings
+                _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height);
+
+                if (!_bitmap.CanCopyTo(destinationInfo.ColorType))
+                {
+                    _bitmap.Dispose();
+
+                    throw new Exception(
+                        $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}");
+                }
+
+                Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
 
-               
-                _bitmap = new SKBitmap(nfo.Width, nfo.Height);
-                if (!_bitmap.CanCopyTo(nfo.ColorType))
+                if (Surface == null)
                 {
                     _bitmap.Dispose();
+
                     throw new Exception(
-                        $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}");
+                        $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}");
                 }
+
+                SurfaceCopyHandler = Disposable.Create(CopySurface);
             }
 
-            public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
+            /// <summary>
+            /// Skia surface.
+            /// </summary>
+            public SKSurface Surface { get; }
 
+            /// <summary>
+            /// Handler to start conversion via surface copy.
+            /// </summary>
+            public IDisposable SurfaceCopyHandler { get; }
+
+            /// <inheritdoc />
             public void Dispose()
             {
-                using (var tmp = _bitmap.Copy(_nfo.ColorType))
-                    tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes);
+                Surface.Dispose();
                 _bitmap.Dispose();
             }
-            
-        }
-
-        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
-        {
-            var fb = _surface.Lock();
-            PixelFormatShim shim = null;
-            SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(),
-                SKAlphaType.Premul);
-            var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ??
-                          (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes))
-                          .CreateSurface();
-            if (surface == null)
-                throw new Exception("Unable to create a surface for pixel format " + fb.Format +
-                                    " or pixel format translator");
-            var canvas = surface.Canvas;
-
 
-
-            canvas.RestoreToCount(0);
-            canvas.Save();
-            canvas.ResetMatrix();
-            return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb);
+            /// <summary>
+            /// Convert and copy surface to a framebuffer.
+            /// </summary>
+            private void CopySurface()
+            {
+                using (var snapshot = Surface.Snapshot())
+                {
+                    snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0,
+                        SKImageCachingHint.Disallow);
+                }
+            }
         }
     }
-}
+}

+ 164 - 6
src/Skia/Avalonia.Skia/GeometryImpl.cs

@@ -1,3 +1,6 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using System;
 using Avalonia.Media;
 using Avalonia.Platform;
@@ -5,14 +8,169 @@ using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    abstract class GeometryImpl : IGeometryImpl
+    /// <summary>
+    /// A Skia implementation of <see cref="IGeometryImpl"/>.
+    /// </summary>
+    public abstract class GeometryImpl : IGeometryImpl
     {
+        private PathCache _pathCache;
+        
+        /// <inheritdoc />
         public abstract Rect Bounds { get; }
         public abstract SKPath EffectivePath { get; }
-        public abstract bool FillContains(Point point);
-        public abstract Rect GetRenderBounds(Pen pen);
-        public abstract IGeometryImpl Intersect(IGeometryImpl geometry);
-        public abstract bool StrokeContains(Pen pen, Point point);
-        public abstract ITransformedGeometryImpl WithTransform(Matrix transform);
+
+        /// <inheritdoc />
+        public bool FillContains(Point point)
+        {
+            return PathContainsCore(EffectivePath, point);
+        }
+
+        /// <inheritdoc />
+        public bool StrokeContains(Pen pen, Point point)
+        {
+            // Skia requires to compute stroke path to check for point containment.
+            // Due to that we are caching using stroke width.
+            // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
+
+            var strokeWidth = (float)(pen?.Thickness ?? 0);
+            
+            if (!_pathCache.HasCacheFor(strokeWidth))
+            {
+                UpdatePathCache(strokeWidth);
+            }
+            
+            return PathContainsCore(_pathCache.CachedStrokePath, point);
+        }
+
+        /// <summary>
+        /// Update path cache for given stroke width.
+        /// </summary>
+        /// <param name="strokeWidth">Stroke width.</param>
+        private void UpdatePathCache(float strokeWidth)
+        {
+            var strokePath = new SKPath();
+
+            // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
+            if (Math.Abs(strokeWidth) < float.Epsilon)
+            {
+                _pathCache.Cache(strokePath, strokeWidth, Bounds);
+            }
+            else
+            {
+                using (var paint = new SKPaint())
+                {
+                    paint.IsStroke = true;
+                    paint.StrokeWidth = strokeWidth;
+                    
+                    paint.GetFillPath(EffectivePath, strokePath);
+
+                    _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Check Skia path if it contains a point.
+        /// </summary>
+        /// <param name="path">Path to check.</param>
+        /// <param name="point">Point.</param>
+        /// <returns>True, if point is contained in a path.</returns>
+        private static bool PathContainsCore(SKPath path, Point point)
+        {
+           return path.Contains((float)point.X, (float)point.Y);
+        }
+
+        /// <inheritdoc />
+        public IGeometryImpl Intersect(IGeometryImpl geometry)
+        {
+            var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect);
+
+            return result == null ? null : new StreamGeometryImpl(result);
+        }
+
+        /// <inheritdoc />
+        public Rect GetRenderBounds(Pen pen)
+        {
+            var strokeWidth = (float)(pen?.Thickness ?? 0);
+            
+            if (!_pathCache.HasCacheFor(strokeWidth))
+            {
+                UpdatePathCache(strokeWidth);
+            }
+            
+            return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
+        }
+        
+        /// <inheritdoc />
+        public ITransformedGeometryImpl WithTransform(Matrix transform)
+        {
+            return new TransformedGeometryImpl(this, transform);
+        }
+
+        /// <summary>
+        /// Invalidate all caches. Call after chaning path contents.
+        /// </summary>
+        protected void InvalidateCaches()
+        {
+            _pathCache.Invalidate();
+        }
+
+        private struct PathCache
+        {
+            private float _cachedStrokeWidth;
+            
+            /// <summary>
+            /// Tolerance for two stroke widths to be deemed equal
+            /// </summary>
+            public const float Tolerance = float.Epsilon;
+            
+            /// <summary>
+            /// Cached contour path.
+            /// </summary>
+            public SKPath CachedStrokePath { get; private set; }
+
+            /// <summary>
+            /// Cached geometry render bounds.
+            /// </summary>
+            public Rect CachedGeometryRenderBounds { get; private set; }
+
+            /// <summary>
+            /// Is cached valid for given stroke width.
+            /// </summary>
+            /// <param name="strokeWidth">Stroke width to check.</param>
+            /// <returns>True, if CachedStrokePath can be used for given stroke width.</returns>
+            public bool HasCacheFor(float strokeWidth)
+            {
+                return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
+            }
+
+            /// <summary>
+            /// Cache path for given stroke width. Takes ownership of a passed path.
+            /// </summary>
+            /// <param name="path">Path to cache.</param>
+            /// <param name="strokeWidth">Stroke width to cache.</param>
+            /// <param name="geometryRenderBounds">Render bounds to use.</param>
+            public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
+            {
+                if (CachedStrokePath != path)
+                {
+                    CachedStrokePath?.Dispose();
+                }
+
+                CachedStrokePath = path;
+                CachedGeometryRenderBounds = geometryRenderBounds;
+                _cachedStrokeWidth = strokeWidth;
+            }
+
+            /// <summary>
+            /// Invalidate cache state.
+            /// </summary>
+            public void Invalidate()
+            {
+                CachedStrokePath?.Dispose();
+                CachedGeometryRenderBounds = Rect.Empty;
+                _cachedStrokeWidth = default(float);
+            }
+        }
     }
 }

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

@@ -0,0 +1,47 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers
+{
+    /// <summary>
+    /// Helps with saving images to stream/file.
+    /// </summary>
+    public static class ImageSavingHelper
+    {
+        /// <summary>
+        /// Save Skia image to a file.
+        /// </summary>
+        /// <param name="image">Image to save</param>
+        /// <param name="fileName">Target file.</param>
+        public static void SaveImage(SKImage image, string fileName)
+        {
+            if (image == null) throw new ArgumentNullException(nameof(image));
+            if (fileName == null) throw new ArgumentNullException(nameof(fileName));
+
+            using (var stream = File.Create(fileName))
+            {
+                SaveImage(image, stream);
+            }
+        }
+
+        /// <summary>
+        /// Save Skia image to a stream.
+        /// </summary>
+        /// <param name="image">Image to save</param>
+        /// <param name="stream">Target stream.</param>
+        public static void SaveImage(SKImage image, Stream stream)
+        {
+            if (image == null) throw new ArgumentNullException(nameof(image));
+            if (stream == null) throw new ArgumentNullException(nameof(stream));
+
+            using (var data = image.Encode())
+            {
+                data.SaveTo(stream);
+            }
+        }
+    }
+}

+ 35 - 0
src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs

@@ -0,0 +1,35 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers
+{
+    /// <summary>
+    /// Helps with resolving pixel formats to Skia color types.
+    /// </summary>
+    public static class PixelFormatHelper
+    {
+        /// <summary>
+        /// Resolve given format to Skia color type.
+        /// </summary>
+        /// <param name="format">Format to resolve.</param>
+        /// <returns>Resolved color type.</returns>
+        public static SKColorType ResolveColorType(PixelFormat? format)
+        {
+            var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
+
+            // TODO: This looks like some leftover hack
+            var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
+            var runtime = runtimePlatform?.GetRuntimeInfo();
+
+            if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux)
+            {
+                colorType = SKColorType.Bgra8888;
+            }
+
+            return colorType;
+        }
+    }
+}

+ 23 - 0
src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs

@@ -0,0 +1,23 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Extended bitmap implementation that allows for drawing it's contents.
+    /// </summary>
+    internal interface IDrawableBitmapImpl : IBitmapImpl
+    {
+        /// <summary>
+        /// Draw bitmap to a drawing context.
+        /// </summary>
+        /// <param name="context">Drawing context.</param>
+        /// <param name="sourceRect">Source rect.</param>
+        /// <param name="destRect">Destination rect.</param>
+        /// <param name="paint">Paint to use.</param>
+        void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint);
+    }
+}

+ 92 - 0
src/Skia/Avalonia.Skia/ImmutableBitmap.cs

@@ -0,0 +1,92 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using Avalonia.Platform;
+using Avalonia.Skia.Helpers;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Immutable Skia bitmap.
+    /// </summary>
+    public class ImmutableBitmap : IDrawableBitmapImpl
+    {
+        private readonly SKImage _image;
+
+        /// <summary>
+        /// Create immutable bitmap from given stream.
+        /// </summary>
+        /// <param name="stream">Stream containing encoded data.</param>
+        public ImmutableBitmap(Stream stream)
+        {
+            using (var skiaStream = new SKManagedStream(stream))
+            {
+                _image = SKImage.FromEncodedData(SKData.Create(skiaStream));
+
+                if (_image == null)
+                {
+                    throw new ArgumentException("Unable to load bitmap from provided data");
+                }
+
+                PixelWidth = _image.Width;
+                PixelHeight = _image.Height;
+            }
+        }
+
+        /// <summary>
+        /// Create immutable bitmap from given pixel data copy.
+        /// </summary>
+        /// <param name="width">Width of data pixels.</param>
+        /// <param name="height">Height of data pixels.</param>
+        /// <param name="stride">Stride of data pixels.</param>
+        /// <param name="format">Format of data pixels.</param>
+        /// <param name="data">Data pixels.</param>
+        public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data)
+        {
+            var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul);
+
+            _image = SKImage.FromPixelCopy(imageInfo, data, stride);
+
+            if (_image == null)
+            {
+                throw new ArgumentException("Unable to create bitmap from provided data");
+            }
+
+            PixelWidth = width;
+            PixelHeight = height;
+        }
+
+        /// <inheritdoc />
+        public int PixelWidth { get; }
+
+        /// <inheritdoc />
+        public int PixelHeight { get; }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            _image.Dispose();
+        }
+
+        /// <inheritdoc />
+        public void Save(string fileName)
+        {
+            ImageSavingHelper.SaveImage(_image, fileName);
+        }
+
+        /// <inheritdoc />
+        public void Save(Stream stream)
+        {
+            ImageSavingHelper.SaveImage(_image, stream);
+        }
+
+        /// <inheritdoc />
+        public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
+        {
+            context.Canvas.DrawImage(_image, sourceRect, destRect, paint);
+        }
+    }
+}

+ 46 - 34
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -1,21 +1,21 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Media;
 using Avalonia.Platform;
-using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    public partial class PlatformRenderInterface : IPlatformRenderInterface
+    /// <summary>
+    /// Skia platform render interface.
+    /// </summary>
+    public class PlatformRenderInterface : IPlatformRenderInterface
     {
-        public IBitmapImpl CreateBitmap(int width, int height)
-        {
-            return CreateRenderTargetBitmap(width, height, 96, 96);
-        }
-
+        /// <inheritdoc />
         public IFormattedTextImpl CreateFormattedText(
             string text,
             Typeface typeface,
@@ -27,27 +27,19 @@ namespace Avalonia.Skia
             return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
         }
 
+        /// <inheritdoc />
         public IStreamGeometryImpl CreateStreamGeometry()
         {
             return new StreamGeometryImpl();
         }
 
-        public IBitmapImpl LoadBitmap(System.IO.Stream stream)
+        /// <inheritdoc />
+        public IBitmapImpl LoadBitmap(Stream stream)
         {
-            using (var s = new SKManagedStream(stream))
-            {
-                var bitmap = SKBitmap.Decode(s);
-                if (bitmap != null)
-                {
-                    return new BitmapImpl(bitmap);
-                }
-                else
-                {
-                    throw new ArgumentException("Unable to load bitmap from provided data");
-                }
-            }
+            return new ImmutableBitmap(stream);
         }
 
+        /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)
         {
             using (var stream = File.OpenRead(fileName))
@@ -56,16 +48,13 @@ namespace Avalonia.Skia
             }
         }
 
+        /// <inheritdoc />
         public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
         {
-            using (var tmp = new SKBitmap())
-            {
-                tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul)
-                    , data, stride);
-                return new BitmapImpl(tmp.Copy());
-            }
+            return new ImmutableBitmap(width, height, stride, format, data);
         }
 
+        /// <inheritdoc />
         public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
             int width,
             int height,
@@ -73,24 +62,47 @@ namespace Avalonia.Skia
             double dpiY)
         {
             if (width < 1)
+            {
                 throw new ArgumentException("Width can't be less than 1", nameof(width));
+            }
+
             if (height < 1)
+            {
                 throw new ArgumentException("Height can't be less than 1", nameof(height));
+            }
+
+            var dpi = new Vector(dpiX, dpiY);
 
-            return new BitmapImpl(width, height, new Vector(dpiX, dpiY));
+            var createInfo = new SurfaceRenderTarget.CreateInfo
+            {
+                Width = width,
+                Height = height,
+                Dpi = dpi,
+                DisableTextLcdRendering = false
+            };
+            
+            return new SurfaceRenderTarget(createInfo);
         }
 
+        /// <inheritdoc />
         public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
-            var fb = surfaces?.OfType<IFramebufferPlatformSurface>().FirstOrDefault();
-            if (fb == null)
-                throw new Exception("Skia backend currently only supports framebuffer render target");
-            return new FramebufferRenderTarget(fb);
+            foreach (var surface in surfaces)
+            {
+                if (surface is IFramebufferPlatformSurface framebufferSurface)
+                {
+                    return new FramebufferRenderTarget(framebufferSurface);
+                }
+            }
+
+            throw new NotSupportedException(
+                "Don't know how to create a Skia render target from any of provided surfaces");
         }
 
+        /// <inheritdoc />
         public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null)
         {
-            return new BitmapImpl(width, height, new Vector(96, 96), format);
+            return new WriteableBitmapImpl(width, height, format);
         }
     }
-}
+}

+ 28 - 0
src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs

@@ -0,0 +1,28 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Controls;
+using Avalonia.Skia;
+
+// ReSharper disable once CheckNamespace
+namespace Avalonia
+{
+    /// <summary>
+    /// Skia appication extensions.
+    /// </summary>
+    public static class SkiaApplicationExtensions
+    {
+        /// <summary>
+        /// Enable Skia renderer.
+        /// </summary>
+        /// <typeparam name="T">Builder type.</typeparam>
+        /// <param name="builder">Builder.</param>
+        /// <param name="preferredBackendType">Preferred backend type.</param>
+        /// <returns>Configure builder.</returns>
+        public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new()
+        {
+            builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia");
+            return builder;
+        }
+    }
+}

+ 15 - 31
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@@ -1,47 +1,31 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using System;
-using System.Collections.Generic;
-using System.Text;
-using Avalonia.Controls;
+using Avalonia.Logging;
 using Avalonia.Platform;
-using Avalonia.Rendering;
-
-namespace Avalonia
-{
-    public static class SkiaApplicationExtensions
-    {
-        public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new()
-        {
-            builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia");
-            return builder;
-        }
-    }
-}
 
 namespace Avalonia.Skia
 {
+    /// <summary>
+    /// Skia platform initializer.
+    /// </summary>
     public static class SkiaPlatform
     {
-        private static bool s_forceSoftwareRendering;
-
+        /// <summary>
+        /// Initialize Skia platform.
+        /// </summary>
         public static void Initialize()
         {
             var renderInterface = new PlatformRenderInterface();
+            
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
         }
 
-        public static bool ForceSoftwareRendering
-        {
-            get { return s_forceSoftwareRendering; }
-            set
-            {
-                s_forceSoftwareRendering = value;
-
-                // TODO: I left this property here as place holder. Do we still need the ability to Force software rendering? 
-                // Is it even possible with SkiaSharp? Perhaps kekekes can answer as part of the HW accel work. 
-                // 
-                throw new NotImplementedException();
-            }
-        }
+        /// <summary>
+        /// Default DPI.
+        /// </summary>
+        public static Vector DefaultDpi => new Vector(96.0f, 96.0f);
     }
 }

+ 4 - 2
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@@ -1,10 +1,12 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using System;
 using Avalonia.Media;
 using Avalonia.Platform;
 using SkiaSharp;
 
-
-namespace Avalonia
+namespace Avalonia.Skia
 {
     public static class SkiaSharpExtensions
     {

+ 69 - 55
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@@ -1,90 +1,103 @@
-using System;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using Avalonia.Media;
 using Avalonia.Platform;
 using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
+    /// <summary>
+    /// A Skia implementation of a <see cref="IStreamGeometryImpl"/>.
+    /// </summary>
+    public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
     {
-        Rect _bounds;
-        SKPath _path;
-
-        public override SKPath EffectivePath => _path;
-
-        public override Rect GetRenderBounds(Pen pen)
+        private Rect _bounds;
+        private readonly SKPath _effectivePath;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        /// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
+        /// <param name="bounds">Precomputed path bounds.</param>
+        public StreamGeometryImpl(SKPath path, Rect bounds)
         {
-            return GetRenderBounds(pen?.Thickness ?? 0);
+            _effectivePath = path;
+            _bounds = bounds;
         }
 
-        public override Rect Bounds => _bounds;
-
-        public IStreamGeometryImpl Clone()
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        /// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
+        public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect())
         {
-            return new StreamGeometryImpl
-            {
-                _path = _path?.Clone(),
-                _bounds = Bounds
-            };
         }
 
-        public IStreamGeometryContextImpl Open()
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty)
         {
-            _path = new SKPath();
-            _path.FillType = SKPathFillType.EvenOdd;
-
-            return new StreamContext(this);
         }
+        
+        /// <inheritdoc />
+        public override SKPath EffectivePath => _effectivePath;
 
-        public override bool FillContains(Point point)
-        {
-            // TODO: Not supported by SkiaSharp yet, so use expanded Rect
-            // return EffectivePath.Contains(point.X, point.Y);
-            return GetRenderBounds(0).Contains(point);
-        }
-
-        public override bool StrokeContains(Pen pen, Point point)
-        {
-            // TODO: Not supported by SkiaSharp yet, so use expanded Rect
-            // return EffectivePath.Contains(point.X, point.Y);
-            return GetRenderBounds(0).Contains(point);
-        }
+        /// <inheritdoc />
+        public override Rect Bounds => _bounds;
 
-        public override IGeometryImpl Intersect(IGeometryImpl geometry)
+        /// <inheritdoc />
+        public IStreamGeometryImpl Clone()
         {
-            throw new NotImplementedException();
+            return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds);
         }
 
-        public override ITransformedGeometryImpl WithTransform(Matrix transform)
+        /// <inheritdoc />
+        public IStreamGeometryContextImpl Open()
         {
-            return new TransformedGeometryImpl(this, transform);
+            return new StreamContext(this);
         }
 
-        private Rect GetRenderBounds(double strokeThickness)
+        /// <summary>
+        /// Create new empty <see cref="SKPath"/>.
+        /// </summary>
+        /// <returns>Empty <see cref="SKPath"/></returns>
+        private static SKPath CreateEmptyPath()
         {
-            // TODO: Calculate properly.
-            return Bounds.Inflate(strokeThickness);
+            return new SKPath
+            {
+                FillType = SKPathFillType.EvenOdd
+            };
         }
 
-        class StreamContext : IStreamGeometryContextImpl
+        /// <summary>
+        /// A Skia implementation of a <see cref="IStreamGeometryContextImpl"/>.
+        /// </summary>
+        private class StreamContext : IStreamGeometryContextImpl
         {
             private readonly StreamGeometryImpl _geometryImpl;
-            private SKPath _path;
+            private readonly SKPath _path;
 
-            Point _currentPoint;
+            /// <summary>
+            /// Initializes a new instance of the <see cref="StreamContext"/> class.
+            /// <param name="geometryImpl">Geometry to operate on.</param>
+            /// </summary>
             public StreamContext(StreamGeometryImpl geometryImpl)
             {
                 _geometryImpl = geometryImpl;
-                _path = _geometryImpl._path;
+                _path = _geometryImpl._effectivePath;
             }
-
+            
+            /// <inheritdoc />
+            /// <remarks>Will update bounds of passed geometry.</remarks>
             public void Dispose()
             {
-                SKRect rc;
-                _path.GetBounds(out rc);
-                _geometryImpl._bounds = rc.ToAvaloniaRect();
+                _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect();
+                _geometryImpl.InvalidateCaches();
             }
 
+            /// <inheritdoc />
             public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
             {
                 _path.ArcTo(
@@ -95,33 +108,33 @@ namespace Avalonia.Skia
                     sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise,
                     (float)point.X,
                     (float)point.Y);
-                _currentPoint = point;
             }
 
+            /// <inheritdoc />
             public void BeginFigure(Point startPoint, bool isFilled)
             {
                 _path.MoveTo((float)startPoint.X, (float)startPoint.Y);
-                _currentPoint = startPoint;
             }
 
+            /// <inheritdoc />
             public void CubicBezierTo(Point point1, Point point2, Point point3)
             {
                 _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
-                _currentPoint = point3;
             }
 
+            /// <inheritdoc />
             public void QuadraticBezierTo(Point point1, Point point2)
             {
                 _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
-                _currentPoint = point2;
             }
 
+            /// <inheritdoc />
             public void LineTo(Point point)
             {
                 _path.LineTo((float)point.X, (float)point.Y);
-                _currentPoint = point;
             }
 
+            /// <inheritdoc />
             public void EndFigure(bool isClosed)
             {
                 if (isClosed)
@@ -130,6 +143,7 @@ namespace Avalonia.Skia
                 }
             }
 
+            /// <inheritdoc />
             public void SetFillRule(FillRule fillRule)
             {
                 _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;

+ 169 - 0
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@@ -0,0 +1,169 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Skia.Helpers;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Skia render target that writes to a surface.
+    /// </summary>
+    public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl
+    {
+        private readonly Vector _dpi;
+        private readonly SKSurface _surface;
+        private readonly SKCanvas _canvas;
+        private readonly bool _disableLcdRendering;
+        
+        /// <summary>
+        /// Create new surface render target.
+        /// </summary>
+        /// <param name="createInfo">Create info.</param>
+        public SurfaceRenderTarget(CreateInfo createInfo)
+        {
+            PixelWidth = createInfo.Width;
+            PixelHeight = createInfo.Height;
+            _dpi = createInfo.Dpi;
+            _disableLcdRendering = createInfo.DisableTextLcdRendering;
+
+            _surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format);
+
+            _canvas = _surface?.Canvas;
+
+            if (_surface == null || _canvas == null)
+            {
+                throw new InvalidOperationException("Failed to create Skia render target surface");
+            }
+        }
+
+        /// <summary>
+        /// Create backing Skia surface.
+        /// </summary>
+        /// <param name="width">Width.</param>
+        /// <param name="height">Height.</param>
+        /// <param name="format">Format.</param>
+        /// <returns></returns>
+        private static SKSurface CreateSurface(int width, int height, PixelFormat? format)
+        {
+            var imageInfo = MakeImageInfo(width, height, format);
+
+            return SKSurface.Create(imageInfo);
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            _canvas.Dispose();
+            _surface.Dispose();
+        }
+
+        /// <inheritdoc />
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        {
+            _canvas.RestoreToCount(-1);
+            _canvas.ResetMatrix();
+            
+            var createInfo = new DrawingContextImpl.CreateInfo
+            {
+                Canvas = _canvas,
+                Dpi = _dpi,
+                VisualBrushRenderer = visualBrushRenderer,
+                DisableTextLcdRendering = _disableLcdRendering
+            };
+
+            return new DrawingContextImpl(createInfo);
+        }
+
+        /// <inheritdoc />
+        public int PixelWidth { get; }
+
+        /// <inheritdoc />
+        public int PixelHeight { get; }
+
+        /// <inheritdoc />
+        public void Save(string fileName)
+        {
+            using (var image = SnapshotImage())
+            {
+                ImageSavingHelper.SaveImage(image, fileName);
+            }
+        }
+
+        /// <inheritdoc />
+        public void Save(Stream stream)
+        {
+            using (var image = SnapshotImage())
+            {
+                ImageSavingHelper.SaveImage(image, stream);
+            }
+        }
+
+        /// <inheritdoc />
+        public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
+        {
+            using (var image = SnapshotImage())
+            {
+                context.Canvas.DrawImage(image, sourceRect, destRect, paint);
+            }
+        }
+        
+        /// <summary>
+        /// Create Skia image snapshot from a surface.
+        /// </summary>
+        /// <returns>Image snapshot.</returns>
+        public SKImage SnapshotImage()
+        {
+            return _surface.Snapshot();
+        }
+
+        /// <summary>
+        /// Create image info for given parameters.
+        /// </summary>
+        /// <param name="width">Width.</param>
+        /// <param name="height">Height.</param>
+        /// <param name="format">Format.</param>
+        /// <returns></returns>
+        private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format)
+        {
+            var colorType = PixelFormatHelper.ResolveColorType(format);
+
+            return new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
+        }
+
+        /// <summary>
+        /// Create info of a surface render target.
+        /// </summary>
+        public struct CreateInfo
+        {
+            /// <summary>
+            /// Width of a render target.
+            /// </summary>
+            public int Width;
+
+            /// <summary>
+            /// Height of a render target.
+            /// </summary>
+            public int Height;
+
+            /// <summary>
+            /// Dpi used when rendering to a surface.
+            /// </summary>
+            public Vector Dpi;
+
+            /// <summary>
+            /// Pixel format of a render target.
+            /// </summary>
+            public PixelFormat? Format;
+
+            /// <summary>
+            /// Render text without Lcd rendering.
+            /// </summary>
+            public bool DisableTextLcdRendering;
+        }
+    }
+}

+ 23 - 39
src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs

@@ -1,59 +1,43 @@
-using System;
-using Avalonia.Media;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using Avalonia.Platform;
 using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
+    /// <summary>
+    /// A Skia implementation of a <see cref="ITransformedGeometryImpl"/>.
+    /// </summary>
+    public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
     {
+        /// <summary>
+        ///  Initializes a new instance of the <see cref="TransformedGeometryImpl"/> class.
+        /// </summary>
+        /// <param name="source">Source geometry.</param>
+        /// <param name="transform">Transform of new geometry.</param>
         public TransformedGeometryImpl(GeometryImpl source, Matrix transform)
         {
             SourceGeometry = source;
             Transform = transform;
-            EffectivePath = source.EffectivePath.Clone();
-            EffectivePath.Transform(transform.ToSKMatrix());
+
+            var transformedPath = source.EffectivePath.Clone();
+            transformedPath.Transform(transform.ToSKMatrix());
+
+            EffectivePath = transformedPath;
+            Bounds = transformedPath.TightBounds.ToAvaloniaRect();
         }
 
+        /// <inheritdoc />
         public override SKPath EffectivePath { get; }
 
+        /// <inheritdoc />
         public IGeometryImpl SourceGeometry { get; }
 
+        /// <inheritdoc />
         public Matrix Transform { get; }
 
-        public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform);
-
-        public override bool FillContains(Point point)
-        {
-            // TODO: Not supported by SkiaSharp yet, so use expanded Rect
-            return GetRenderBounds(0).Contains(point);
-        }
-
-        public override Rect GetRenderBounds(Pen pen)
-        {
-            return GetRenderBounds(pen.Thickness);
-        }
-
-        public override IGeometryImpl Intersect(IGeometryImpl geometry)
-        {
-            throw new NotImplementedException();
-        }
-
-        public override bool StrokeContains(Pen pen, Point point)
-        {
-            // TODO: Not supported by SkiaSharp yet, so use expanded Rect
-            return GetRenderBounds(0).Contains(point);
-        }
-
-        public override ITransformedGeometryImpl WithTransform(Matrix transform)
-        {
-            return new TransformedGeometryImpl(this, transform);
-        }
-
-        public Rect GetRenderBounds(double strokeThickness)
-        {
-            // TODO: Calculate properly.
-            return Bounds.Inflate(strokeThickness);
-        }
+        /// <inheritdoc />
+        public override Rect Bounds { get; }
     }
 }

+ 8 - 1
src/Skia/Avalonia.Skia/TypefaceCache.cs

@@ -1,3 +1,7 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Media;
@@ -5,7 +9,10 @@ using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    static class TypefaceCache
+    /// <summary>
+    /// Cache for Skia typefaces.
+    /// </summary>
+    internal static class TypefaceCache
     {
         public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name);
         static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> Cache = new Dictionary<string, Dictionary<FontKey, SKTypeface>>();

+ 151 - 0
src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs

@@ -0,0 +1,151 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using Avalonia.Platform;
+using Avalonia.Skia.Helpers;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Skia based writeable bitmap.
+    /// </summary>
+    public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl
+    {
+        private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc;
+        private readonly SKBitmap _bitmap;
+
+        /// <summary>
+        /// Create new writeable bitmap.
+        /// </summary>
+        /// <param name="width">Width.</param>
+        /// <param name="height">Height.</param>
+        /// <param name="format">Format.</param>
+        public WriteableBitmapImpl(int width, int height, PixelFormat? format = null)
+        {
+            PixelHeight = height;
+            PixelWidth = width;
+
+            var colorType = PixelFormatHelper.ResolveColorType(format);
+            
+            var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
+            
+            if (runtimePlatform != null)
+            {
+                _bitmap = new SKBitmap();
+
+                var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
+                var blob = runtimePlatform.AllocBlob(nfo.BytesSize);
+
+                _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob);
+            }
+            else
+            {
+                _bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul);
+            }
+
+            _bitmap.Erase(SKColor.Empty);
+        }
+
+        /// <inheritdoc />
+        public int PixelWidth { get; }
+
+        /// <inheritdoc />
+        public int PixelHeight { get; }
+
+        /// <inheritdoc />
+        public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
+        {
+            context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            _bitmap.Dispose();
+        }
+
+        /// <inheritdoc />
+        public void Save(Stream stream)
+        {
+            using (var image = GetSnapshot())
+            {
+                ImageSavingHelper.SaveImage(image, stream);
+            }
+        }
+
+        /// <inheritdoc />
+        public void Save(string fileName)
+        {
+            using (var image = GetSnapshot())
+            {
+                ImageSavingHelper.SaveImage(image, fileName);
+            }
+        }
+
+        /// <inheritdoc />
+        public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap);
+
+        /// <summary>
+        /// Get snapshot as image.
+        /// </summary>
+        /// <returns>Image snapshot.</returns>
+        public SKImage GetSnapshot()
+        {
+            return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
+        }
+
+        /// <summary>
+        /// Release given unmanaged blob.
+        /// </summary>
+        /// <param name="address">Blob address.</param>
+        /// <param name="ctx">Blob.</param>
+        private static void ReleaseProc(IntPtr address, object ctx)
+        {
+            ((IUnmanagedBlob)ctx).Dispose();
+        }
+
+        /// <summary>
+        /// Framebuffer for bitmap.
+        /// </summary>
+        private class BitmapFramebuffer : ILockedFramebuffer
+        {
+            private SKBitmap _bitmap;
+
+            /// <summary>
+            /// Create framebuffer from given bitmap.
+            /// </summary>
+            /// <param name="bitmap">Bitmap.</param>
+            public BitmapFramebuffer(SKBitmap bitmap)
+            {
+                _bitmap = bitmap;
+            }
+
+            /// <inheritdoc />
+            public void Dispose()
+            {
+                _bitmap = null;
+            }
+            
+            /// <inheritdoc />
+            public IntPtr Address => _bitmap.GetPixels();
+
+            /// <inheritdoc />
+            public int Width => _bitmap.Width;
+
+            /// <inheritdoc />
+            public int Height => _bitmap.Height;
+
+            /// <inheritdoc />
+            public int RowBytes => _bitmap.RowBytes;
+
+            /// <inheritdoc />
+            public Vector Dpi { get; } = SkiaPlatform.DefaultDpi;
+
+            /// <inheritdoc />
+            public PixelFormat Format => _bitmap.ColorType.ToPixelFormat();
+        }
+    }
+}

+ 7 - 27
src/Skia/Avalonia.Skia/readme.md

@@ -1,42 +1,22 @@
-TODO:
-
-BitmapImpl 
-- constructor from Width/Height
-- Save
-
-StreamGeometryImpl
-- Hit testing in Geometry missing as SkiaSharp does not expose this
-
 DrawingContextImpl
 - Alpha support missing as SkiaSharp does not expose this
 - Gradient Shader caching?
-- TileBrushes
 - Pen Dash styles
 
 Formatted Text Rendering 
-- minor polish
+- Minor polish
 
-RenderTarget
-- Figure out a cleaner implementation across all platforms
-- HW acceleration
+Linux
+- Need gpu platform implementation
 
-App Bootstrapping
-- Cleanup the testapplications across all platforms
-- Add a cleaner Fluent API for the subsystems
-	- ie.    app.UseDirect2D()    (via platform specific extension methods)
+macOS
+- Need gpu platform implementation
 
 Android
 - Not tested at all yet
 
 iOS
-- Get GLView working again. See HW above
-
-Win32
-- Cleanup the unmanaged methods (BITMAPINFO) if possible
+- Not tested at all yet
 
 General
-- Cleanup/eliminate obsolete files
-- Finish cleanup of the many Test Applications
-- Get Skia Unit Tests passing
-
-
+- Get Skia Unit Tests passing (most of the issues are related to antialiasing)

+ 28 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -321,6 +321,24 @@ namespace Avalonia.Win32.Interop
             WS_EX_NOACTIVATE = 0x08000000
         }
 
+        [Flags]
+        public enum ClassStyles : uint
+        {
+            CS_VREDRAW = 0x0001,
+            CS_HREDRAW = 0x0002,
+            CS_DBLCLKS = 0x0008,
+            CS_OWNDC = 0x0020,
+            CS_CLASSDC = 0x0040,
+            CS_PARENTDC = 0x0080,
+            CS_NOCLOSE = 0x0200,
+            CS_SAVEBITS = 0x0800,
+            CS_BYTEALIGNCLIENT = 0x1000,
+            CS_BYTEALIGNWINDOW = 0x2000,
+            CS_GLOBALCLASS = 0x4000,
+            CS_IME = 0x00010000,
+            CS_DROPSHADOW = 0x00020000
+        }
+
         public enum WindowsMessage : uint
         {
             WM_NULL = 0x0000,
@@ -1194,6 +1212,16 @@ namespace Avalonia.Win32.Interop
             public IntPtr hIconSm;
         }
 
+        [Flags]
+        public enum OpenFileNameFlags
+        {
+            OFN_ALLOWMULTISELECT = 0x00000200,
+            OFN_EXPLORER = 0x00080000,
+            OFN_HIDEREADONLY = 0x00000004,
+            OFN_NOREADONLYRETURN = 0x00008000,
+            OFN_OVERWRITEPROMPT = 0x00000002
+        }
+        
         public enum HRESULT : uint
         {
             S_FALSE = 0x0001,

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

@@ -724,7 +724,7 @@ namespace Avalonia.Win32
             UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
             {
                 cbSize = Marshal.SizeOf<UnmanagedMethods.WNDCLASSEX>(),
-                style = 0,
+                style = (int)(ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW), // Unique DC helps with performance when using Gpu based rendering
                 lpfnWndProc = _wndProcDelegate,
                 hInstance = UnmanagedMethods.GetModuleHandle(null),
                 hCursor = DefaultCursor,

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

@@ -67,7 +67,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         
         [Theory]
         [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888),
-#if SKIA
+#if AVALONIA_SKIA
              InlineData(PixelFormat.Rgb565)
 #endif
             ]

+ 2 - 10
tests/Avalonia.RenderTests/Media/ImageBrushTests.cs

@@ -281,12 +281,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             await RenderToFile(target);
             CompareImages();
         }
-
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
+        
         [Fact]
-#endif
         public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest()
         {
             Decorator target = new Decorator
@@ -309,12 +305,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             await RenderToFile(target);
             CompareImages();
         }
-
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
+        
         [Fact]
-#endif
         public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest()
         {
             Decorator target = new Decorator

+ 2 - 10
tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs

@@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush")
         {
         }
-
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
+        
         [Fact]
-#endif
         public async Task LinearGradientBrush_RedBlue_Horizontal_Fill()
         {
             Decorator target = new Decorator
@@ -52,12 +48,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             await RenderToFile(target);
             CompareImages();
         }
-
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
+        
         [Fact]
-#endif
         public async Task LinearGradientBrush_RedBlue_Vertical_Fill()
         {
             Decorator target = new Decorator

+ 1 - 5
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush")
         {
         }
-
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
+        
         [Fact]
-#endif
         public async Task RadialGradientBrush_RedBlue()
         {
             Decorator target = new Decorator

+ 1 - 5
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@@ -270,12 +270,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             await RenderToFile(target);
             CompareImages();
         }
-
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
+        
         [Fact]
-#endif
         public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest()
         {
             Decorator target = new Decorator

+ 22 - 0
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
+  <PropertyGroup>
+    <TargetFrameworks>netcoreapp2.0</TargetFrameworks>
+  </PropertyGroup>
+  <Import Project="..\..\build\UnitTests.NetCore.targets" />
+  <Import Project="..\..\build\Moq.props" />
+  <Import Project="..\..\build\XUnit.props" />
+  <Import Project="..\..\build\Rx.props" />
+  <Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
+    <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+    <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
+  </ItemGroup>
+</Project>

+ 79 - 0
tests/Avalonia.Skia.UnitTests/HitTesting.cs

@@ -0,0 +1,79 @@
+using Avalonia.Controls.Shapes;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests
+{
+    public class HitTesting
+    {
+        [Fact]
+        public void Hit_Test_Should_Respect_Fill()
+        {
+            using (AvaloniaLocator.EnterScope())
+            {
+                SkiaPlatform.Initialize();
+
+                var root = new TestRoot
+                {
+                    Width = 100,
+                    Height = 100,
+                    Child = new Ellipse
+                    {
+                        Width = 100,
+                        Height = 100,
+                        Fill = Brushes.Red,
+                        HorizontalAlignment = HorizontalAlignment.Center,
+                        VerticalAlignment = VerticalAlignment.Center
+                    }
+                };
+
+                root.Renderer = new DeferredRenderer(root, null);
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(root.DesiredSize));
+
+                var outsideResult = root.Renderer.HitTest(new Point(10, 10), root, null);
+                var insideResult = root.Renderer.HitTest(new Point(50, 50), root, null);
+
+                Assert.Empty(outsideResult);
+                Assert.Equal(new[] {root.Child}, insideResult);
+            }
+        }
+
+        [Fact]
+        public void Hit_Test_Should_Respect_Stroke()
+        {
+            using (AvaloniaLocator.EnterScope())
+            {
+                SkiaPlatform.Initialize();
+
+                var root = new TestRoot
+                {
+                    Width = 100,
+                    Height = 100,
+                    Child = new Ellipse
+                    {
+                        Width = 100,
+                        Height = 100,
+                        Stroke = Brushes.Red,
+                        StrokeThickness = 5,
+                        HorizontalAlignment = HorizontalAlignment.Center,
+                        VerticalAlignment = VerticalAlignment.Center
+                    }
+                };
+
+                root.Renderer = new DeferredRenderer(root, null);
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(root.DesiredSize));
+
+                var outsideResult = root.Renderer.HitTest(new Point(50, 50), root, null);
+                var insideResult = root.Renderer.HitTest(new Point(1, 50), root, null);
+
+                Assert.Empty(outsideResult);
+                Assert.Equal(new[] { root.Child }, insideResult);
+            }
+        }
+    }
+}

+ 8 - 0
tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs

@@ -0,0 +1,8 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Reflection;
+using Xunit;
+
+// Don't run tests in parallel.
+[assembly: CollectionBehavior(DisableTestParallelization = true)]

二進制
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png


二進制
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png


二進制
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png


二進制
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png


二進制
tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png