Selaa lähdekoodia

Merge pull request #903 from kekekeks/bitmaps

Implemented WritableBitmap and bitmap creation by copying from in-memory data
Nikita Tsukanov 8 vuotta sitten
vanhempi
sitoutus
e3d24cf11a
39 muutettua tiedostoa jossa 448 lisäystä ja 43 poistoa
  1. 1 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  2. 0 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  3. 1 0
      src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs
  4. 0 15
      src/Avalonia.Controls/Platform/Surfaces/PixelFormat.cs
  5. 4 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  6. 14 0
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  7. 22 0
      src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs
  8. 1 1
      src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs
  9. 21 0
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  10. 16 0
      src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs
  11. 9 0
      src/Avalonia.Visuals/Platform/PixelFormat.cs
  12. 10 0
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  13. 1 0
      src/Gtk/Avalonia.Gtk/FramebufferManager.cs
  14. 1 0
      src/Gtk/Avalonia.Gtk3/FramebufferManager.cs
  15. 41 4
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  16. 2 15
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  17. 15 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  18. 24 0
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  19. 1 0
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  20. 11 0
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  21. 27 3
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  22. 47 0
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs
  23. 11 0
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  24. 1 0
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  25. 1 0
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  26. 3 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  27. 2 1
      src/Windows/Avalonia.Win32/WindowFramebuffer.cs
  28. 1 1
      src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs
  29. 10 0
      tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs
  30. 1 0
      tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
  31. 139 0
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  32. 10 0
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  33. BIN
      tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png
  34. BIN
      tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png
  35. BIN
      tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png
  36. BIN
      tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png
  37. BIN
      tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png
  38. BIN
      tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png
  39. BIN
      tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@@ -2,7 +2,7 @@ using System;
 using System.Runtime.InteropServices;
 using Android.Runtime;
 using Android.Views;
-using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {

+ 0 - 2
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -59,8 +59,6 @@
     <Compile Include="IScrollable.cs" />
     <Compile Include="Platform\IWindowBaseImpl.cs" />
     <Compile Include="Platform\Surfaces\IFramebufferPlatformSurface.cs" />
-    <Compile Include="Platform\Surfaces\ILockedFramebuffer.cs" />
-    <Compile Include="Platform\Surfaces\PixelFormat.cs" />
     <Compile Include="PointEventArgs.cs" />
     <Compile Include="Embedding\EmbeddableControlRoot.cs" />
     <Compile Include="Platform\IEmbeddableWindowImpl.cs" />

+ 1 - 0
src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using Avalonia.Platform;
 
 namespace Avalonia.Controls.Platform.Surfaces
 {

+ 0 - 15
src/Avalonia.Controls/Platform/Surfaces/PixelFormat.cs

@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Avalonia.Controls.Platform.Surfaces
-{
-    public enum PixelFormat
-    {
-        Rgb565,
-        Rgba8888,
-        Bgra8888
-    }
-}

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

@@ -69,6 +69,7 @@
     <Compile Include="Media\BrushMappingMode.cs" />
     <Compile Include="Media\Color.cs" />
     <Compile Include="Media\Colors.cs" />
+    <Compile Include="Media\Imaging\WritableBitmap.cs" />
     <Compile Include="Media\TextWrapping.cs" />
     <Compile Include="Media\TransformGroup.cs" />
     <Compile Include="Media\DashStyle.cs" />
@@ -102,7 +103,10 @@
     <Compile Include="Media\Geometry.cs" />
     <Compile Include="Media\IDrawingContext.cs" />
     <Compile Include="Platform\ExportRenderingSubsystemAttribute.cs" />
+    <Compile Include="Platform\ILockedFramebuffer.cs" />
     <Compile Include="Platform\IModuleEnvironmentChecker.cs" />
+    <Compile Include="Platform\IWritableBitmapImpl.cs" />
+    <Compile Include="Platform\PixelFormat.cs" />
     <Compile Include="Rendering\IRenderer.cs" />
     <Compile Include="Rendering\IRendererFactory.cs" />
     <Compile Include="Rendering\IRenderLoop.cs" />

+ 14 - 0
src/Avalonia.Visuals/Media/Imaging/Bitmap.cs

@@ -41,6 +41,20 @@ namespace Avalonia.Media.Imaging
             PlatformImpl = impl;
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Bitmap"/> class.
+        /// </summary>
+        /// <param name="format">Pixel format</param>
+        /// <param name="data">Pointer to source bytes</param>
+        /// <param name="width">Bitmap width</param>
+        /// <param name="height">Bitmap height</param>
+        /// <param name="stride">Bytes per row</param>
+        public Bitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        {
+            PlatformImpl = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
+                .LoadBitmap(format, data, width, height, stride);
+        }
+
         /// <summary>
         /// Gets the width of the bitmap, in pixels.
         /// </summary>

+ 22 - 0
src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Imaging
+{
+    /// <summary>
+    /// Holds a writable bitmap image.
+    /// </summary>
+    public class WritableBitmap : Bitmap
+    {
+        public WritableBitmap(int width, int height, PixelFormat? format = null) 
+            : base(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateWritableBitmap(width, height, format))
+        {
+        }
+
+        public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl).Lock();
+    }
+}

+ 1 - 1
src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs → src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs

@@ -1,6 +1,6 @@
 using System;
 
-namespace Avalonia.Controls.Platform.Surfaces
+namespace Avalonia.Platform
 {
     public interface ILockedFramebuffer : IDisposable
     {

+ 21 - 0
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -1,6 +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.IO;
 using Avalonia.Media;
@@ -55,6 +56,15 @@ namespace Avalonia.Platform
         /// <returns>An <see cref="IRenderTargetBitmapImpl"/>.</returns>
         IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height);
 
+        /// <summary>
+        /// Creates a writable bitmap implementation.
+        /// </summary>
+        /// <param name="width">The width of the bitmap.</param>
+        /// <param name="height">The height of the bitmap.</param>
+        /// <param name="format">Pixel format (optional).</param>
+        /// <returns>An <see cref="IWritableBitmapImpl"/>.</returns>
+        IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null);
+
         /// <summary>
         /// Loads a bitmap implementation from a file..
         /// </summary>
@@ -68,5 +78,16 @@ namespace Avalonia.Platform
         /// <param name="stream">The stream to read the bitmap from.</param>
         /// <returns>An <see cref="IBitmapImpl"/>.</returns>
         IBitmapImpl LoadBitmap(Stream stream);
+
+        /// <summary>
+        /// Loads a bitmap implementation from a pixels in memory..
+        /// </summary>
+        /// <param name="format">Pixel format</param>
+        /// <param name="data">Pointer to source bytes</param>
+        /// <param name="width">Bitmap width</param>
+        /// <param name="height">Bitmap height</param>
+        /// <param name="stride">Bytes per row</param>
+        /// <returns>An <see cref="IBitmapImpl"/>.</returns>
+        IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride);
     }
 }

+ 16 - 0
src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Avalonia.Platform
+{
+    /// <summary>
+    /// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.WritableBitmap"/>.
+    /// </summary>
+    public interface IWritableBitmapImpl : IBitmapImpl
+    {
+        ILockedFramebuffer Lock();
+    }
+}

+ 9 - 0
src/Avalonia.Visuals/Platform/PixelFormat.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Platform
+{
+    public enum PixelFormat
+    {
+        Rgb565,
+        Rgba8888,
+        Bgra8888
+    }
+}

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

@@ -91,5 +91,15 @@ namespace Avalonia.Cairo
             Gtk.Application.Init();
             return new Gtk.Invisible().CreatePangoContext();
         }
+
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        {
+            throw new NotSupportedException("No proper control over pixel format with Cairo, use Skia backend instead");
+        }
+
+        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt)
+        {
+            throw new NotSupportedException("No proper support with Cairo, use Skia backend instead");
+        }
     }
 }

+ 1 - 0
src/Gtk/Avalonia.Gtk/FramebufferManager.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
 
 namespace Avalonia.Gtk
 {

+ 1 - 0
src/Gtk/Avalonia.Gtk3/FramebufferManager.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
 
 namespace Avalonia.Gtk3
 {

+ 41 - 4
src/Skia/Avalonia.Skia/BitmapImpl.cs

@@ -9,7 +9,7 @@ using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    class BitmapImpl : IRenderTargetBitmapImpl
+    class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl
     {
         public SKBitmap Bitmap { get; private set; }
 
@@ -20,11 +20,11 @@ namespace Avalonia.Skia
             PixelWidth = bm.Width;
         }
 
-        public BitmapImpl(int width, int height)
+        public BitmapImpl(int width, int height, PixelFormat? fmt = null)
         {
             PixelHeight = height;
             PixelWidth = width;
-            var colorType = SKImageInfo.PlatformColorType;
+            var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
             var runtime = AvaloniaLocator.Current?.GetService<IRuntimePlatform>()?.GetRuntimeInfo();
             if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux)
                 colorType = SKColorType.Bgra8888;
@@ -38,10 +38,21 @@ namespace Avalonia.Skia
 
         public void Save(string fileName)
         {
+            
 #if DESKTOP
+            if(Bitmap.ColorType != SKColorType.Bgra8888)
+            {
+                using (var tmp = new BitmapImpl(Bitmap.Copy(SKColorType.Bgra8888)))
+                    tmp.Save(fileName);
+                return;
+            }
+
             IntPtr length;
             using (var sdb = new System.Drawing.Bitmap(PixelWidth, PixelHeight, Bitmap.RowBytes,
-                System.Drawing.Imaging.PixelFormat.Format32bppArgb, Bitmap.GetPixels(out length)))
+                
+                System.Drawing.Imaging.PixelFormat.Format32bppArgb,
+                
+                Bitmap.GetPixels(out length)))
                 sdb.Save(fileName);
 #else
             //SkiaSharp doesn't expose image encoders yet
@@ -96,5 +107,31 @@ namespace Avalonia.Skia
                 data.SaveTo(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 Size Dpi { get; } = new Size(96, 96);
+            public PixelFormat Format => _bmp.ColorType.ToPixelFormat();
+        }
+
+        public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap);
     }
 }

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

@@ -22,19 +22,6 @@ namespace Avalonia.Skia
             //Nothing to do here, since we don't own framebuffer
         }
 
-
-        SKColorType TranslatePixelFormat(PixelFormat fmt)
-        {
-            if(fmt == PixelFormat.Rgb565)
-                return SKColorType.Rgb565;
-            if(fmt == PixelFormat.Bgra8888)
-                return SKColorType.Bgra8888;
-            if (fmt == PixelFormat.Rgba8888)
-                return SKColorType.Rgba8888;
-            throw new ArgumentException("Unknown pixel format: " + fmt);
-        }
-
-
         class PixelFormatShim : IDisposable
         {
             private readonly SKImageInfo _nfo;
@@ -73,8 +60,8 @@ namespace Avalonia.Skia
         {
             var fb = _surface.Lock();
             PixelFormatShim shim = null;
-            SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, TranslatePixelFormat(fb.Format),
-                SKAlphaType.Opaque);
+            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();

+ 15 - 0
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -52,6 +52,16 @@ namespace Avalonia.Skia
             }
         }
 
+        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());
+            }
+        }
+
         public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
         {
             return new Renderer(root, renderLoop);
@@ -74,5 +84,10 @@ namespace Avalonia.Skia
                 throw new Exception("Skia backend currently only supports framebuffer render target");
             return new FramebufferRenderTarget(fb);
         }
+
+        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)
+        {
+            return new BitmapImpl(width, height, format);
+        }
     }
 }

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

@@ -1,4 +1,6 @@
+using System;
 using Avalonia.Media;
+using Avalonia.Platform;
 using SkiaSharp;
 
 
@@ -44,6 +46,28 @@ namespace Avalonia.Skia
             return new SKColor(c.R, c.G, c.B, c.A);
         }
 
+        public static SKColorType ToSkColorType(this PixelFormat fmt)
+        {
+            if (fmt == PixelFormat.Rgb565)
+                return SKColorType.Rgb565;
+            if (fmt == PixelFormat.Bgra8888)
+                return SKColorType.Bgra8888;
+            if (fmt == PixelFormat.Rgba8888)
+                return SKColorType.Rgba8888;
+            throw new ArgumentException("Unknown pixel format: " + fmt);
+        }
+
+        public static PixelFormat ToPixelFormat(this SKColorType fmt)
+        {
+            if (fmt == SKColorType.Rgb565)
+                return PixelFormat.Rgb565;
+            if (fmt == SKColorType.Bgra8888)
+                return PixelFormat.Bgra8888;
+            if (fmt == SKColorType.Rgba8888)
+                return PixelFormat.Rgba8888;
+            throw new ArgumentException("Unknown pixel format: " + fmt);
+        }
+
         public static SKShaderTileMode ToSKShaderTileMode(this Media.GradientSpreadMethod m)
         {
             switch (m)

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

@@ -70,6 +70,7 @@
     <Compile Include="Media\Imaging\D2DBitmapImpl.cs" />
     <Compile Include="Media\Imaging\RenderTargetBitmapImpl.cs" />
     <Compile Include="Media\Imaging\WicBitmapImpl.cs" />
+    <Compile Include="Media\Imaging\WritableWicBitmapImpl.cs" />
     <Compile Include="Media\RadialGradientBrushImpl.cs" />
     <Compile Include="Media\LinearGradientBrushImpl.cs" />
     <Compile Include="Media\AvaloniaTextRenderer.cs" />

+ 11 - 0
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -10,6 +10,7 @@ using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Direct2D1.Media.Imaging;
 using Avalonia.Rendering;
 
 namespace Avalonia
@@ -119,6 +120,11 @@ namespace Avalonia.Direct2D1
             return new RenderTargetBitmapImpl(s_imagingFactory, s_d2D1Device.Factory, width, height);
         }
 
+        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)
+        {
+            return new WritableWicBitmapImpl(s_imagingFactory, width, height, format);
+        }
+
         public IStreamGeometryImpl CreateStreamGeometry()
         {
             return new StreamGeometryImpl();
@@ -133,5 +139,10 @@ namespace Avalonia.Direct2D1
         {
             return new WicBitmapImpl(s_imagingFactory, stream);
         }
+
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        {
+            return new WicBitmapImpl(s_imagingFactory, format, data, width, height, stride);
+        }
     }
 }

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

@@ -4,6 +4,9 @@
 using System;
 using System.IO;
 using Avalonia.Platform;
+using Avalonia.Win32.Interop;
+using PixelFormat  = SharpDX.WIC.PixelFormat;
+using APixelFormat  = Avalonia.Platform.PixelFormat;
 using SharpDX.WIC;
 
 namespace Avalonia.Direct2D1.Media
@@ -53,17 +56,38 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="factory">The WIC imaging factory to use.</param>
         /// <param name="width">The width of the bitmap.</param>
         /// <param name="height">The height of the bitmap.</param>
-        public WicBitmapImpl(ImagingFactory factory, int width, int height)
+        /// <param name="pixelFormat">Pixel format</param>
+        public WicBitmapImpl(ImagingFactory factory, int width, int height, APixelFormat? pixelFormat = null)
         {
+            if (!pixelFormat.HasValue)
+                pixelFormat = APixelFormat.Bgra8888;
+
             _factory = factory;
+            PixelFormat = pixelFormat;
             WicImpl = new Bitmap(
                 factory,
                 width,
                 height,
-                PixelFormat.Format32bppPBGRA,
+                pixelFormat.Value.ToWic(),
                 BitmapCreateCacheOption.CacheOnLoad);
         }
 
+        public WicBitmapImpl(ImagingFactory factory, Platform.PixelFormat format, IntPtr data, int width, int height, int stride)
+        {
+            WicImpl = new Bitmap(factory, width, height, format.ToWic(), BitmapCreateCacheOption.CacheOnDemand);
+            PixelFormat = format;
+            using (var l = WicImpl.Lock(BitmapLockFlags.Write))
+            {
+                for (var row = 0; row < height; row++)
+                {
+                    UnmanagedMethods.CopyMemory(new IntPtr(l.Data.DataPointer.ToInt64() + row * l.Stride),
+                        new IntPtr(data.ToInt64() + row * stride), (uint) l.Data.Pitch);
+                }
+            }
+        }
+
+        protected APixelFormat? PixelFormat { get; }
+
         /// <summary>
         /// Gets the width of the bitmap, in pixels.
         /// </summary>
@@ -95,7 +119,7 @@ namespace Avalonia.Direct2D1.Media
             if (_direct2D == null)
             {
                 FormatConverter converter = new FormatConverter(_factory);
-                converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA);
+                converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA);
                 _direct2D = SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
             }
 

+ 47 - 0
src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+using SharpDX.WIC;
+using PixelFormat = Avalonia.Platform.PixelFormat;
+
+namespace Avalonia.Direct2D1.Media.Imaging
+{
+    class WritableWicBitmapImpl : WicBitmapImpl, IWritableBitmapImpl
+    {
+        public WritableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) 
+            : base(factory, width, height, pixelFormat)
+        {
+        }
+
+        class LockedBitmap : ILockedFramebuffer
+        {
+            private readonly BitmapLock _lock;
+            private readonly PixelFormat _format;
+
+            public LockedBitmap(BitmapLock l, PixelFormat format)
+            {
+                _lock = l;
+                _format = format;
+            }
+
+
+            public void Dispose()
+            {
+                _lock.Dispose();
+            }
+
+            public IntPtr Address => _lock.Data.DataPointer;
+            public int Width => _lock.Size.Width;
+            public int Height => _lock.Size.Height;
+            public int RowBytes => _lock.Stride;
+            public Size Dpi { get; } = new Size(96, 96);
+            public PixelFormat Format => _format;
+
+        }
+
+        public ILockedFramebuffer Lock() => new LockedBitmap(WicImpl.Lock(BitmapLockFlags.Write), PixelFormat.Value);
+    }
+}

+ 11 - 0
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@@ -88,6 +88,17 @@ namespace Avalonia.Direct2D1
                 return CapStyle.Triangle;
         }
 
+        public static Guid ToWic(this Platform.PixelFormat format)
+        {
+            if (format == Platform.PixelFormat.Rgb565)
+                return SharpDX.WIC.PixelFormat.Format16bppBGR565;
+            if (format == Platform.PixelFormat.Bgra8888)
+                return SharpDX.WIC.PixelFormat.Format32bppPBGRA;
+            if (format == Platform.PixelFormat.Rgba8888)
+                return SharpDX.WIC.PixelFormat.Format32bppPRGBA;
+            throw new ArgumentException("Unknown pixel format");
+        }
+
         /// <summary>
         /// Converts a pen to a Direct2D stroke style.
         /// </summary>

+ 1 - 0
src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs

@@ -9,6 +9,7 @@ using Avalonia.Win32.Interop;
 using SharpDX;
 using SharpDX.Direct2D1;
 using SharpDX.DXGI;
+using PixelFormat = SharpDX.Direct2D1.PixelFormat;
 using AlphaMode = SharpDX.Direct2D1.AlphaMode;
 using Device = SharpDX.Direct2D1.Device;
 using Factory = SharpDX.Direct2D1.Factory;

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

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
 using Avalonia.Win32.Interop;
 
 namespace Avalonia.Win32

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

@@ -909,6 +909,9 @@ namespace Avalonia.Win32.Interop
             uint dwMaximumSizeLow,
             string lpName);
 
+        [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
+        public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
+
         public enum MONITOR
         {
             MONITOR_DEFAULTTONULL = 0x00000000,

+ 2 - 1
src/Windows/Avalonia.Win32/WindowFramebuffer.cs

@@ -1,8 +1,9 @@
 using System;
 using System.Runtime.InteropServices;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
 using Avalonia.Win32.Interop;
-using PixelFormat = Avalonia.Controls.Platform.Surfaces.PixelFormat;
+using PixelFormat = Avalonia.Platform.PixelFormat;
 
 namespace Avalonia.Win32
 {

+ 1 - 1
src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using System.Text;
-using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
 using CoreGraphics;
 using UIKit;
 

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

@@ -365,6 +365,16 @@ namespace Avalonia.Input.UnitTests
                 throw new NotImplementedException();
             }
 
+            public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+            {
+                throw new NotImplementedException();
+            }
+
+            public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt)
+            {
+                throw new NotImplementedException();
+            }
+
             class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl
             {
                 private MockStreamGeometryContext _impl = new MockStreamGeometryContext();

+ 1 - 0
tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems

@@ -10,6 +10,7 @@
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="$(MSBuildThisFileDirectory)GeometryClippingTests.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Media\BitmapTests.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)OpacityMaskTests.cs" />
     <Compile Include="Media\FormattedTextImplTests.cs" />
     <Compile Include="Controls\ImageTests.cs" />

+ 139 - 0
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@@ -0,0 +1,139 @@
+// 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 System.Runtime.InteropServices;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Controls.Shapes;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Xunit;
+
+#if AVALONIA_CAIRO
+namespace Avalonia.Cairo.RenderTests.Media
+#elif AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+    public class BitmapTests : TestBase
+    {
+        public BitmapTests()
+            : base(@"Media\Bitmap")
+        {
+            Directory.CreateDirectory(OutputPath);
+        }
+
+        class Framebuffer : ILockedFramebuffer, IFramebufferPlatformSurface
+        {
+            public Framebuffer(PixelFormat fmt, int width, int height)
+            {
+                Format = fmt;
+                var bpp = fmt == PixelFormat.Rgb565 ? 2 : 4;
+                Width = width;
+                Height = height;
+                RowBytes = bpp * width;
+                Address = Marshal.AllocHGlobal(Height * RowBytes);
+            }
+
+            public IntPtr Address { get; }
+
+            public Size Dpi { get; } = new Size(96, 96);
+
+            public PixelFormat Format { get; }
+
+            public int Height { get; }
+
+            public int RowBytes { get; }
+
+            public int Width { get; }
+
+            public void Dispose()
+            {
+                //no-op
+            }
+
+            public ILockedFramebuffer Lock()
+            {
+                return this;
+            }
+
+            public void Deallocate() => Marshal.FreeHGlobal(Address);
+        }
+
+
+#if AVALONIA_SKIA
+        [Theory]
+#else
+        [Theory(Skip = "Framebuffer not supported")]
+#endif
+        [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgb565)]
+        public void FramebufferRenderResultsShouldBeUsableAsBitmap(PixelFormat fmt)
+        {
+            var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt;
+            var fb = new Framebuffer(fmt, 80, 80);
+            var r = Avalonia.AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+            using (var target = r.CreateRenderTarget(new object[] { fb }))
+            using (var ctx = target.CreateDrawingContext())
+            {
+                ctx.PushOpacity(0.8);
+                ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100));
+                ctx.FillRectangle(Brushes.Crimson, new Rect(20, 0, 20, 100));
+                ctx.FillRectangle(Brushes.Gold, new Rect(40, 0, 20, 100));
+            }
+
+            var bmp = new Bitmap(fmt, fb.Address, fb.Width, fb.Height, fb.RowBytes);
+            fb.Deallocate();
+            using (var rtb = new RenderTargetBitmap(100, 100))
+            {
+                using (var ctx = rtb.CreateDrawingContext())
+                {
+                    ctx.FillRectangle(Brushes.Blue, new Rect(0, 0, 100, 100));
+                    ctx.FillRectangle(Brushes.Pink, new Rect(0, 20, 100, 10));
+
+                    var rc = new Rect(0, 0, 60, 60);
+                    ctx.DrawImage(bmp, 1, rc, rc);
+                }
+                rtb.Save(System.IO.Path.Combine(OutputPath, testName + ".out.png"));
+            }
+            CompareImages(testName);
+        }
+
+#if AVALONIA_CAIRO
+        //wontfix
+#else
+        [Theory]
+#endif
+        [InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgba8888)]
+        public void WritableBitmapShouldBeUsable(PixelFormat fmt)
+        {
+            var writableBitmap = new WritableBitmap(256, 256, fmt);
+
+            var data = new int[256 * 256];
+            for (int y = 0; y < 256; y++)
+                for (int x = 0; x < 256; x++)
+                    data[y * 256 + x] =(int)((uint)(x + (y << 8)) | 0xFF000000u);
+
+
+            using (var l = writableBitmap.Lock())
+            {
+                for(var r = 0; r<256; r++)
+                {
+                    Marshal.Copy(data, r * 256, new IntPtr(l.Address.ToInt64() + r * l.RowBytes), 256);
+                }
+            }
+
+
+            var name = nameof(WritableBitmapShouldBeUsable) + "_" + fmt;
+
+            writableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png"));
+            CompareImages(name);
+
+        }
+    }
+}

+ 10 - 0
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -45,6 +45,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt)
+        {
+            throw new NotImplementedException();
+        }
+
         class MockStreamGeometry : IStreamGeometryImpl
         {
             private MockStreamGeometryContext _impl = new MockStreamGeometryContext();

BIN
tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png


BIN
tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png


BIN
tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png


BIN
tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png


BIN
tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png


BIN
tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png


BIN
tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png