Jelajahi Sumber

Framebuffer extensions tests wip

Krzysztof Krysiński 1 tahun lalu
induk
melakukan
58dca5b728
23 mengubah file dengan 301 tambahan dan 35 penghapusan
  1. 1 1
      src/Directory.Build.props
  2. 2 2
      src/PixiEditor.AvaloniaUI.Desktop/Program.cs
  3. 7 1
      src/PixiEditor.AvaloniaUI/Helpers/Extensions/BitmapExtensions.cs
  4. 12 4
      src/PixiEditor.AvaloniaUI/Helpers/Extensions/LockedFramebufferExtensions.cs
  5. 19 0
      src/PixiEditor.AvaloniaUI/Helpers/UI/RenderOptionsBindable.cs
  6. 2 2
      src/PixiEditor.AvaloniaUI/Helpers/WriteableBitmapHelpers.cs
  7. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/Public/DocumentOperationsModule.cs
  8. 1 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/IReferenceLayerHandler.cs
  9. 1 0
      src/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj
  10. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs
  11. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/Document/ReferenceLayerViewModel.cs
  12. 5 5
      src/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyReferenceLayer.cs
  14. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/ReferenceLayer.cs
  15. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ReferenceLayerChanges/DeleteReferenceLayer_Change.cs
  16. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Root/ReferenceLayerChanges/SetReferenceLayer_Change.cs
  17. 79 0
      src/PixiEditor.Tests/AvaloniaTestRunner.cs
  18. 75 0
      src/PixiEditor.Tests/FramebufferExtensionTests.cs
  19. 1 0
      src/PixiEditor.Tests/GlobalUsings.cs
  20. 32 0
      src/PixiEditor.Tests/PixiEditor.Tests.csproj
  21. 45 0
      src/PixiEditor.sln
  22. 1 1
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  23. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		<AvaloniaVersion>11.0.3</AvaloniaVersion>
+		<AvaloniaVersion>11.0.4</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 2 - 2
src/PixiEditor.AvaloniaUI.Desktop/Program.cs

@@ -2,9 +2,9 @@
 using Avalonia;
 using PixiEditor.AvaloniaUI;
 
-namespace PixiEditor.Avalonia.Desktop;
+namespace PixiEditor.AvaloniaUI.Desktop;
 
-class Program
+public class Program
 {
     // Initialization code. Don't use any Avalonia, third-party APIs or any
     // SynchronizationContext-reliant code before AppMain is called: things aren't initialized

+ 7 - 1
src/PixiEditor.AvaloniaUI/Helpers/Extensions/BitmapExtensions.cs

@@ -12,6 +12,12 @@ public static class BitmapExtensions
         return ExtractPixels(source, out _);
     }
 
+    /// <summary>
+    ///     Extracts pixels from bitmap and returns them as byte array.
+    /// </summary>
+    /// <param name="source">Bitmap to extract pixels from.</param>
+    /// <param name="address">Address of pinned array of pixels.</param>
+    /// <returns>Byte array of pixels.</returns>
     public static byte[] ExtractPixels(this Bitmap source, out IntPtr address)
     {
         var size = source.PixelSize;
@@ -37,6 +43,6 @@ public static class BitmapExtensions
         var address = Marshal.UnsafeAddrOfPinnedArrayElement(target, 0);
 
         source.CopyPixels(new PixelRect(0, 0, size.Width, size.Height), address, bufferSize, stride);
-        return new WriteableBitmap(PixelFormat.Bgra8888, AlphaFormat.Premul, address, size, new Vector(96, 96), stride);
+        return new WriteableBitmap(PixelFormats.Bgra8888, AlphaFormat.Premul, address, size, new Vector(96, 96), stride);
     }
 }

+ 12 - 4
src/PixiEditor.AvaloniaUI/Helpers/Extensions/LockedFramebufferExtensions.cs

@@ -18,14 +18,22 @@ public static class LockedFramebufferExtensions
     {
         unsafe
         {
+            if(framebuffer.Format != PixelFormat.Bgra8888)
+                throw new ArgumentException("Only Bgra8888 is supported");
+
             var bytesPerPixel = framebuffer.Format.BitsPerPixel / 8; //TODO: check if bits per pixel is correct
             var zero = (byte*)framebuffer.Address;
             var offset = framebuffer.RowBytes * y + bytesPerPixel * x;
-            return Color.FromArgb(255, zero[offset + 2], zero[offset + 1], zero[offset]);
+            byte a = zero[offset + 3];
+            byte r = zero[offset + 2];
+            byte g = zero[offset + 1];
+            byte b = zero[offset];
+
+            return Color.FromArgb(a, r, g, b);
         }
     }
 
-    public static void WritePixels(this ILockedFramebuffer framebuffer, RectI rectI, byte[] pbgra8888Bytes)
+    public static void WritePixels(this ILockedFramebuffer framebuffer, RectI rectI, byte[] pixelBytes)
     {
         //TODO: Idk if this is correct
         Span<byte> pixels = framebuffer.GetPixels();
@@ -38,7 +46,7 @@ public static class LockedFramebufferExtensions
         int startY = Math.Max(0, rectI.Y);
         int endY = Math.Min(framebuffer.Size.Height, rectI.Y + rectI.Height);
 
-        int bytePerPixel = 4; // BGRA8888 has 4 bytes per pixel
+        int bytePerPixel = framebuffer.Format.BitsPerPixel / 8;
 
         for (int y = startY; y < endY; y++)
         {
@@ -48,7 +56,7 @@ public static class LockedFramebufferExtensions
 
             int srcRowStartIndex = (y - rectI.Y) * rectI.Width * bytePerPixel;
 
-            pbgra8888Bytes.AsSpan(srcRowStartIndex, endOffset - startOffset).CopyTo(pixels.Slice(startOffset));
+            pixelBytes.AsSpan(srcRowStartIndex, endOffset - startOffset).CopyTo(pixels.Slice(startOffset));
         }
     }
 

+ 19 - 0
src/PixiEditor.AvaloniaUI/Helpers/UI/RenderOptionsBindable.cs

@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+
+namespace PixiEditor.AvaloniaUI.Helpers.UI;
+
+public class RenderOptionsBindable
+{
+    public static readonly AttachedProperty<BitmapInterpolationMode> BitmapInterpolationModeProperty =
+        AvaloniaProperty.RegisterAttached<RenderOptionsBindable, Visual, BitmapInterpolationMode>("BitmapInterpolationMode");
+
+    public static void SetBitmapInterpolationMode(Visual obj, BitmapInterpolationMode value)
+    {
+        obj.SetValue(BitmapInterpolationModeProperty, value);
+        RenderOptions.SetBitmapInterpolationMode(obj, value);
+    }
+
+    public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual obj) => obj.GetValue(BitmapInterpolationModeProperty);
+}

+ 2 - 2
src/PixiEditor.AvaloniaUI/Helpers/WriteableBitmapHelpers.cs

@@ -10,11 +10,11 @@ namespace PixiEditor.AvaloniaUI.Helpers;
 
 internal static class WriteableBitmapHelpers
 {
-    public static WriteableBitmap FromPbgra8888Array(byte[] pbgra8888, VecI size)
+    public static WriteableBitmap FromBgra8888Array(byte[] bgra8888, VecI size)
     {
         WriteableBitmap bitmap = new WriteableBitmap(new PixelSize(size.X, size.Y), new Vector(96, 96), PixelFormats.Bgra8888, AlphaFormat.Premul);
         using var frameBuffer = bitmap.Lock();
-        frameBuffer.WritePixels(new RectI(0, 0, size.X, size.Y), pbgra8888);
+        frameBuffer.WritePixels(new RectI(0, 0, size.X, size.Y), bgra8888);
         return bitmap;
     }
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -509,14 +509,14 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// Imports a reference layer from a Pbgra Int32 array
     /// </summary>
     /// <param name="imageSize">The size of the image</param>
-    public void ImportReferenceLayer(ImmutableArray<byte> imagePbgra32Bytes, VecI imageSize)
+    public void ImportReferenceLayer(ImmutableArray<byte> imageBgra8888Bytes, VecI imageSize)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
 
         RectD referenceImageRect = new RectD(VecD.Zero, Document.SizeBindable).AspectFit(new RectD(VecD.Zero, imageSize));
         ShapeCorners corners = new ShapeCorners(referenceImageRect);
-        Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imagePbgra32Bytes, imageSize));
+        Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imageBgra8888Bytes, imageSize));
     }
 
     /// <summary>

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IReferenceLayerHandler.cs

@@ -16,6 +16,6 @@ public interface IReferenceLayerHandler : IHandler
     public void SetReferenceLayerIsVisible(bool infoIsVisible);
     public void TransformReferenceLayer(ShapeCorners infoCorners);
     public void DeleteReferenceLayer();
-    public void SetReferenceLayer(ImmutableArray<byte> imagePbgra8888Bytes, VecI infoImageSize, ShapeCorners infoShape);
+    public void SetReferenceLayer(ImmutableArray<byte> imageBgra8888Bytes, VecI infoImageSize, ShapeCorners infoShape);
     public void SetReferenceLayerTopMost(bool infoIsTopMost);
 }

+ 1 - 0
src/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj

@@ -22,6 +22,7 @@
 
     <ItemGroup>
         <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
+        <PackageReference Include="Avalonia.Headless" Version="11.0.4" />
         <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
         <PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
         <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -52,7 +52,7 @@ internal partial class DocumentViewModel
 
         var surface = new Surface(new VecI(layer.ImageSize.X, layer.ImageSize.Y));
         
-        surface.DrawBytes(surface.Size, layer.ImagePbgra32Bytes.ToArray(), ColorType.Bgra8888, AlphaType.Premul);
+        surface.DrawBytes(surface.Size, layer.ImageBgra8888Bytes.ToArray(), ColorType.Bgra8888, AlphaType.Premul);
 
         var encoder = new UniversalFileEncoder(EncodedImageFormat.Png);
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Document/ReferenceLayerViewModel.cs

@@ -107,9 +107,9 @@ internal class ReferenceLayerViewModel : ObservableObject, IReferenceLayerHandle
 
     public void RaiseShowHighestChanged() => OnPropertyChanged(nameof(ShowHighest));
     
-    public void SetReferenceLayer(ImmutableArray<byte> imagePbgra8888Bytes, VecI imageSize, ShapeCorners shape)
+    public void SetReferenceLayer(ImmutableArray<byte> imageBgra8888Bytes, VecI imageSize, ShapeCorners shape)
     {
-        ReferenceBitmap = WriteableBitmapHelpers.FromPbgra8888Array(imagePbgra8888Bytes.ToArray(), imageSize);
+        ReferenceBitmap = WriteableBitmapHelpers.FromBgra8888Array(imageBgra8888Bytes.ToArray(), imageSize);
         referenceShape = shape;
         isVisible = true;
         isTransforming = false;

+ 5 - 5
src/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml

@@ -23,6 +23,7 @@
     xmlns:main="clr-namespace:PixiEditor.AvaloniaUI.Views.Main"
     xmlns:viewModels1="clr-namespace:PixiEditor.ViewModels"
     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+    xmlns:ui1="clr-namespace:PixiEditor.AvaloniaUI.Helpers.UI"
     mc:Ignorable="d"
     x:Name="vpUc"
     d:DesignHeight="450"
@@ -158,8 +159,8 @@
                 <Grid>
                     <Canvas
                         ZIndex="{Binding Document.ReferenceLayerViewModel.ShowHighest, Converter={converters:BoolToIntConverter}}"
-                        IsHitTestVisible="{Binding Document.ReferenceLayerViewModel.IsTransforming}">
-                        <!--TODO: RenderOptions.BitmapInterpolationMode="{Binding ReferenceLayerScale, Converter={converters:ScaleToBitmapScalingModeConverter}}"-->
+                        IsHitTestVisible="{Binding Document.ReferenceLayerViewModel.IsTransforming}"
+                        ui1:RenderOptionsBindable.BitmapInterpolationMode="{Binding ReferenceLayerScale, Converter={converters:ScaleToBitmapScalingModeConverter}}">
                         <Image
                             Focusable="False"
                             Width="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap.Size.Width}"
@@ -167,14 +168,13 @@
                             Source="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap, Mode=OneWay}"
                             IsVisible="{Binding Document.ReferenceLayerViewModel.IsVisibleBindable}"
                             SizeChanged="OnReferenceImageSizeChanged"
-
                             FlowDirection="LeftToRight">
-                            <Image.RenderTransform>
+                            <!--<Image.RenderTransform>
                                 <TransformGroup>
                                     <MatrixTransform
                                         Matrix="{Binding Document.ReferenceLayerViewModel.ReferenceTransformMatrix}" />
                                 </TransformGroup>
-                            </Image.RenderTransform>
+                            </Image.RenderTransform>-->
                             <Image.Styles>
                                 <!--TODO: Implement this-->
                                 <!--<Style>

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyReferenceLayer.cs

@@ -4,7 +4,7 @@ using PixiEditor.DrawingApi.Core.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 public interface IReadOnlyReferenceLayer
 {
-    public ImmutableArray<byte> ImagePbgra32Bytes { get; }
+    public ImmutableArray<byte> ImageBgra8888Bytes { get; }
     public VecI ImageSize { get; }
     public ShapeCorners Shape { get; }
     public bool IsVisible { get; }

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/ReferenceLayer.cs

@@ -6,21 +6,21 @@ namespace PixiEditor.ChangeableDocument.Changeables;
 
 public class ReferenceLayer : IReadOnlyReferenceLayer
 {
-    public ImmutableArray<byte> ImagePbgra32Bytes { get; }
+    public ImmutableArray<byte> ImageBgra8888Bytes { get; }
     public VecI ImageSize { get; }
     public ShapeCorners Shape { get; set; }
     public bool IsVisible { get; set; } = true;
     public bool IsTopMost { get; set; }
     
-    public ReferenceLayer(ImmutableArray<byte> imagePbgra32Bytes, VecI imageSize, ShapeCorners shape)
+    public ReferenceLayer(ImmutableArray<byte> imageBgra8888Bytes, VecI imageSize, ShapeCorners shape)
     {
-        ImagePbgra32Bytes = imagePbgra32Bytes;
+        ImageBgra8888Bytes = imageBgra8888Bytes;
         ImageSize = imageSize;
         Shape = shape;
     }
 
     public ReferenceLayer Clone()
     {
-        return new ReferenceLayer(ImagePbgra32Bytes, ImageSize, Shape);
+        return new ReferenceLayer(ImageBgra8888Bytes, ImageSize, Shape);
     }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/ReferenceLayerChanges/DeleteReferenceLayer_Change.cs

@@ -29,7 +29,7 @@ internal class DeleteReferenceLayer_Change : Change
     {
         target.ReferenceLayer = lastReferenceLayer!.Clone();
         return new SetReferenceLayer_ChangeInfo(
-            target.ReferenceLayer.ImagePbgra32Bytes,
+            target.ReferenceLayer.ImageBgra8888Bytes,
             target.ReferenceLayer.ImageSize,
             target.ReferenceLayer.Shape);
     }

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Root/ReferenceLayerChanges/SetReferenceLayer_Change.cs

@@ -7,16 +7,16 @@ namespace PixiEditor.ChangeableDocument.Changes.Root.ReferenceLayerChanges;
 
 internal class SetReferenceLayer_Change : Change
 {
-    private readonly ImmutableArray<byte> imagePbgra32Bytes;
+    private readonly ImmutableArray<byte> imageBgra8888Bytes;
     private readonly VecI imageSize;
     private readonly ShapeCorners shape;
 
     private ReferenceLayer? lastReferenceLayer;
     
     [GenerateMakeChangeAction]
-    public SetReferenceLayer_Change(ShapeCorners shape, ImmutableArray<byte> imagePbgra32Bytes, VecI imageSize)
+    public SetReferenceLayer_Change(ShapeCorners shape, ImmutableArray<byte> imageBgra8888Bytes, VecI imageSize)
     {
-        this.imagePbgra32Bytes = imagePbgra32Bytes;
+        this.imageBgra8888Bytes = imageBgra8888Bytes;
         this.imageSize = imageSize;
         this.shape = shape;
     }
@@ -29,9 +29,9 @@ internal class SetReferenceLayer_Change : Change
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
-        target.ReferenceLayer = new ReferenceLayer(imagePbgra32Bytes, imageSize, shape);
+        target.ReferenceLayer = new ReferenceLayer(imageBgra8888Bytes, imageSize, shape);
         ignoreInUndo = false;
-        return new SetReferenceLayer_ChangeInfo(imagePbgra32Bytes, imageSize, shape);
+        return new SetReferenceLayer_ChangeInfo(imageBgra8888Bytes, imageSize, shape);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
@@ -40,7 +40,7 @@ internal class SetReferenceLayer_Change : Change
         if (lastReferenceLayer is null)
             return new DeleteReferenceLayer_ChangeInfo();
         return new SetReferenceLayer_ChangeInfo(
-            lastReferenceLayer.ImagePbgra32Bytes,
+            lastReferenceLayer.ImageBgra8888Bytes,
             lastReferenceLayer.ImageSize,
             lastReferenceLayer.Shape);
     }

+ 79 - 0
src/PixiEditor.Tests/AvaloniaTestRunner.cs

@@ -0,0 +1,79 @@
+using System.Reflection;
+using Avalonia.Headless;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+[assembly:TestFramework("PixiEditor.Tests.AvaloniaTestRunner", "PixiEditor.Tests")]
+[assembly:CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = false, MaxParallelThreads = 1)]
+namespace PixiEditor.Tests
+{
+    public class AvaloniaTestRunner : XunitTestFramework
+    {
+        public AvaloniaTestRunner(IMessageSink messageSink) : base(messageSink)
+        {
+        }
+
+        protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
+            => new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
+
+
+        class Executor : XunitTestFrameworkExecutor
+        {
+            public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider,
+                IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider,
+                diagnosticMessageSink)
+            {
+            }
+
+            protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases,
+                IMessageSink executionMessageSink,
+                ITestFrameworkExecutionOptions executionOptions)
+            {
+                executionOptions.SetValue("xunit.execution.DisableParallelization", false);
+                using (var assemblyRunner = new Runner(
+                    TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
+                    executionOptions)) await assemblyRunner.RunAsync();
+            }
+        }
+
+        class Runner : XunitTestAssemblyRunner
+        {
+            public Runner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases,
+                IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
+                ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
+                executionMessageSink, executionOptions)
+            {
+            }
+
+
+            protected override void SetupSyncContext(int maxParallelThreads)
+            {
+                var tcs = new TaskCompletionSource<SynchronizationContext>();
+                new Thread(() =>
+                {
+                    try
+                    {
+                        PixiEditor.AvaloniaUI.Desktop.Program.BuildAvaloniaApp()
+                            .UseHeadless(new AvaloniaHeadlessPlatformOptions { FrameBufferFormat = PixelFormat.Bgra8888, UseHeadlessDrawing = true })
+                            .SetupWithoutStarting();
+                        tcs.SetResult(SynchronizationContext.Current);
+                    }
+                    catch (Exception e)
+                    {
+                        tcs.SetException(e);
+                    }
+                    Dispatcher.UIThread.MainLoop(CancellationToken.None);
+                })
+                {
+                    IsBackground = true
+                }.Start();
+
+                SynchronizationContext.SetSynchronizationContext(tcs.Task.Result);
+            }
+
+
+        }
+    }
+}

+ 75 - 0
src/PixiEditor.Tests/FramebufferExtensionTests.cs

@@ -0,0 +1,75 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using PixiEditor.AvaloniaUI.Helpers.Extensions;
+using PixiEditor.DrawingApi.Core.Numerics;
+using SkiaSharp;
+
+namespace PixiEditor.Tests;
+
+public class FramebufferExtensionTests
+{
+    [Fact]
+    public void TestThatExtractPixelsFromBitmapReturnsCorrectAmountOfPixels()
+    {
+        var bitmap = new WriteableBitmap(new PixelSize(10, 10), new Vector(96, 96), PixelFormats.Bgra8888);
+        var pixels = bitmap.ExtractPixels();
+        Assert.Equal(400, pixels.Length);
+    }
+
+    [Theory]
+    [InlineData(255, 0, 0, 255)]
+    [InlineData(0, 255, 0, 255)]
+    [InlineData(0, 0, 255, 255)]
+    [InlineData(255, 255, 255, 255)]
+    [InlineData(0, 0, 0, 255)]
+    [InlineData(255, 255, 255, 0)]
+    [InlineData(0, 0, 0, 0)]
+    public void TestThatWritePixelSetsCorrectColor(byte r, byte g, byte b, byte a)
+    {
+        var bitmap = new WriteableBitmap(new PixelSize(1, 1), new Vector(96, 96), PixelFormats.Bgra8888);
+        using var framebuffer = bitmap.Lock();
+        framebuffer.WritePixel(0, 0, Color.FromArgb(a, r, g, b));
+        var pixels = framebuffer.GetPixels();
+        Assert.Equal(r, pixels[2]);
+        Assert.Equal(g, pixels[1]);
+        Assert.Equal(b, pixels[0]);
+        Assert.Equal(a, pixels[3]);
+    }
+
+    [Theory]
+    [InlineData(255, 0, 0, 255)]
+    [InlineData(0, 255, 0, 255)]
+    [InlineData(0, 0, 255, 255)]
+    [InlineData(255, 255, 255, 255)]
+    [InlineData(0, 0, 0, 255)]
+    [InlineData(255, 255, 255, 0)]
+    [InlineData(0, 0, 0, 0)]
+    public void TestThatGetPixelsReturnsCorrectColor(byte r, byte g, byte b, byte a)
+    {
+        WriteableBitmap bitmap = new WriteableBitmap(new PixelSize(1, 1), new Vector(96, 96), PixelFormats.Bgra8888, AlphaFormat.Premul);
+        using var framebuffer = bitmap.Lock();
+        framebuffer.WritePixel(0, 0, Color.FromArgb(a, r, g, b));
+        var color = framebuffer.GetPixel(0, 0);
+        Assert.Equal(r, color.R);
+        Assert.Equal(g, color.G);
+        Assert.Equal(b, color.B);
+        Assert.Equal(a, color.A);
+    }
+
+    /*[Fact]
+    public void TestThatExtractPixelsFromBitmapReturnsCorrectBgra8888ByteSequence()
+    {
+        var bitmap = new WriteableBitmap(new PixelSize(4, 1), new Vector(96, 96), PixelFormats.Bgra8888);
+        using var framebuffer = bitmap.Lock();
+        framebuffer.WritePixel();
+        var pixels = bitmap.ExtractPixels();
+
+        Assert.Equal(4, pixels.Length);
+        Assert.Equal(0, pixels[0]);
+        Assert.Equal(0, pixels[1]);
+    }*/
+}

+ 1 - 0
src/PixiEditor.Tests/GlobalUsings.cs

@@ -0,0 +1 @@
+global using Xunit;

+ 32 - 0
src/PixiEditor.Tests/PixiEditor.Tests.csproj

@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Avalonia" Version="11.0.4" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
+        <PackageReference Include="xunit" Version="2.4.2"/>
+        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+            <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
+        <PackageReference Include="coverlet.collector" Version="3.2.0">
+            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+            <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
+    </ItemGroup>
+
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.AvaloniaUI.Desktop\PixiEditor.AvaloniaUI.Desktop.csproj" />
+    </ItemGroup>
+
+</Project>

+ 45 - 0
src/PixiEditor.sln

@@ -78,6 +78,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.AvaloniaUI.Brows
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.AvaloniaUI.Desktop", "PixiEditor.AvaloniaUI.Desktop\PixiEditor.AvaloniaUI.Desktop.csproj", "{8F4FFC91-BE9F-4476-A372-FBD952865F15}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Tests", "PixiEditor.Tests\PixiEditor.Tests.csproj", "{427CE098-4B13-4E46-8C66-D924140B6CAE}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -1297,6 +1299,48 @@ Global
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Steam|x64.Build.0 = Debug|Any CPU
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Steam|x86.ActiveCfg = Debug|Any CPU
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Steam|x86.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Debug|x64.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Debug|x86.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevSteam|Any CPU.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevSteam|Any CPU.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevSteam|x86.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.DevSteam|x86.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX|x64.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.MSIX|x86.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Release|x64.ActiveCfg = Release|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Release|x64.Build.0 = Release|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Release|x86.ActiveCfg = Release|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Release|x86.Build.0 = Release|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Steam|x64.Build.0 = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{427CE098-4B13-4E46-8C66-D924140B6CAE}.Steam|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1330,6 +1374,7 @@ Global
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{19704B2E-5EED-47CA-9258-89F246F50F19} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{427CE098-4B13-4E46-8C66-D924140B6CAE} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}

+ 1 - 1
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -146,7 +146,7 @@ internal class DocumentUpdater
 
     private void ProcessSetReferenceLayer(SetReferenceLayer_ChangeInfo info)
     {
-        doc.ReferenceLayerViewModel.InternalSetReferenceLayer(info.ImagePbgra8888Bytes, info.ImageSize, info.Shape);
+        doc.ReferenceLayerViewModel.InternalSetReferenceLayer(info.ImageRgba64Bytes, info.ImageSize, info.Shape);
     }
     
     private void ProcessReferenceLayerTopMost(ReferenceLayerTopMost_ChangeInfo info)

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs

@@ -52,7 +52,7 @@ internal partial class DocumentViewModel
 
         var surface = new Surface(new VecI(layer.ImageSize.X, layer.ImageSize.Y));
         
-        surface.DrawBytes(surface.Size, layer.ImagePbgra32Bytes.ToArray(), ColorType.Bgra8888, AlphaType.Premul);
+        surface.DrawBytes(surface.Size, layer.ImageBgra8888Bytes.ToArray(), ColorType.Bgra8888, AlphaType.Premul);
 
         var encoder = new PngBitmapEncoder();