Explorar o código

Merge branch 'refs/heads/avalonia-rewrite' into analytics

# Conflicts:
#	src/PixiEditor/Views/MainWindow.axaml.cs
CPKreuz hai 1 ano
pai
achega
84852f496a
Modificáronse 75 ficheiros con 1518 adicións e 474 borrados
  1. 1 1
      src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs
  2. 7 3
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  3. 12 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  4. 18 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  5. 46 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  6. 7 1
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  7. 8 0
      src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs
  8. 3 1
      src/PixiEditor.Desktop/Program.cs
  9. 3 1
      src/PixiEditor.DrawingApi.Core/Bridge/IDrawingBackend.cs
  10. 1 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IBitmapImplementation.cs
  11. 2 1
      src/PixiEditor.DrawingApi.Core/Bridge/Operations/ISurfaceImplementation.cs
  12. 5 0
      src/PixiEditor.DrawingApi.Core/Surfaces/Bitmap.cs
  13. 5 0
      src/PixiEditor.DrawingApi.Core/Surfaces/DrawingSurface.cs
  14. 2 0
      src/PixiEditor.DrawingApi.Core/Surfaces/ImageData/ImageInfo.cs
  15. 1 1
      src/PixiEditor.DrawingApi.Core/Surfaces/Pixmap.cs
  16. 126 0
      src/PixiEditor.DrawingApi.Core/Texture.cs
  17. 10 0
      src/PixiEditor.DrawingApi.Skia/Exceptions/GrContextAlreadyInitializedException.cs
  18. 26 0
      src/PixiEditor.DrawingApi.Skia/Extensions/DrawingBackendExtensions.cs
  19. 1 1
      src/PixiEditor.DrawingApi.Skia/Implementations/SKObjectImplementation.cs
  20. 17 6
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs
  21. 16 2
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs
  22. 79 9
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaSurfaceImplementation.cs
  23. 39 0
      src/PixiEditor.DrawingApi.Skia/RenderGraphicsContext.cs
  24. 27 10
      src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs
  25. 123 109
      src/PixiEditor.UI.Common/Controls/ProgressBar.axaml
  26. 8 1
      src/PixiEditor/Data/Localization/Languages/en.json
  27. 1 1
      src/PixiEditor/Helpers/MarkupExtensions/EnumExtension.cs
  28. 3 3
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  29. 17 6
      src/PixiEditor/Models/Files/ImageFileType.cs
  30. 1 1
      src/PixiEditor/Models/Files/IoFileType.cs
  31. 3 1
      src/PixiEditor/Models/Files/PixiFileType.cs
  32. 19 3
      src/PixiEditor/Models/Files/VideoFileType.cs
  33. 2 2
      src/PixiEditor/Models/Handlers/IDocument.cs
  34. 1 1
      src/PixiEditor/Models/Handlers/IKeyFrameHandler.cs
  35. 1 1
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  36. 1 1
      src/PixiEditor/Models/Handlers/IReferenceLayerHandler.cs
  37. 2 2
      src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs
  38. 29 0
      src/PixiEditor/Models/IO/ExportJob.cs
  39. 8 6
      src/PixiEditor/Models/IO/Exporter.cs
  40. 26 8
      src/PixiEditor/Models/IO/Importer.cs
  41. 39 20
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  42. 128 103
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  43. 8 8
      src/PixiEditor/Models/UserData/RecentlyOpenedDocument.cs
  44. 24 19
      src/PixiEditor/Styles/Templates/KeyFrame.axaml
  45. 5 5
      src/PixiEditor/Styles/Templates/NodeView.axaml
  46. 3 3
      src/PixiEditor/Styles/Templates/TimelineGroupHeader.axaml
  47. 2 2
      src/PixiEditor/ViewModels/Dock/LayoutManager.cs
  48. 26 12
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  49. 2 2
      src/PixiEditor/ViewModels/Document/KeyFrameViewModel.cs
  50. 2 2
      src/PixiEditor/ViewModels/Document/ReferenceLayerViewModel.cs
  51. 5 4
      src/PixiEditor/ViewModels/Document/StructureMemberViewModel.cs
  52. 2 2
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  53. 2 1
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  54. 21 8
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  55. 3 3
      src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs
  56. 62 24
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs
  57. 48 0
      src/PixiEditor/Views/Dialogs/ProgressDialog.cs
  58. 17 0
      src/PixiEditor/Views/Dialogs/ProgressPopup.axaml
  59. 51 0
      src/PixiEditor/Views/Dialogs/ProgressPopup.axaml.cs
  60. 4 3
      src/PixiEditor/Views/Layers/FolderControl.axaml
  61. 2 2
      src/PixiEditor/Views/Layers/LayerControl.axaml
  62. 1 1
      src/PixiEditor/Views/Layers/ReferenceLayer.axaml
  63. 3 1
      src/PixiEditor/Views/Main/Tools/ToolPickerButton.axaml
  64. 3 3
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml
  65. 9 11
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs
  66. 2 2
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  67. 40 4
      src/PixiEditor/Views/MainWindow.axaml.cs
  68. 2 2
      src/PixiEditor/Views/Nodes/NodeView.cs
  69. 21 2
      src/PixiEditor/Views/Overlays/ReferenceLayerOverlay.cs
  70. 11 3
      src/PixiEditor/Views/Palettes/PaletteColorControl.axaml.cs
  71. 1 0
      src/PixiEditor/Views/Palettes/PaletteViewer.axaml.cs
  72. 27 10
      src/PixiEditor/Views/Rendering/Scene.cs
  73. 206 0
      src/PixiEditor/Views/Visuals/TextureControl.cs
  74. 26 0
      src/PixiEditor/Views/Visuals/TextureImage.cs
  75. 3 3
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml

+ 1 - 1
src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs

@@ -4,5 +4,5 @@ namespace PixiEditor.AnimationRenderer.Core;
 
 public interface IAnimationRenderer
 {
-    public Task<bool> RenderAsync(List<Image> imageStream, string outputPath);
+    public Task<bool> RenderAsync(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
 }

+ 7 - 3
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -16,7 +16,7 @@ public class FFMpegRenderer : IAnimationRenderer
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
 
-    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath)
+    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback = null)
     {
         string path = "ThirdParty/{0}/ffmpeg";
 #if WINDOWS
@@ -64,7 +64,9 @@ public class FFMpegRenderer : IAnimationRenderer
                 });
 
             var outputArgs = GetProcessorForFormat(args, outputPath, paletteTempPath);
-            var result = await outputArgs.ProcessAsynchronously();
+            TimeSpan totalTimeSpan = TimeSpan.FromSeconds(frames.Count / (float)FrameRate);
+            var result = await outputArgs.CancellableThrough(cancellationToken)
+                .NotifyOnProgress(progressCallback, totalTimeSpan).ProcessAsynchronously();
             
             if (RequiresPaletteGeneration())
             {
@@ -120,7 +122,9 @@ public class FFMpegRenderer : IAnimationRenderer
             .OutputToFile(outputPath, true, options =>
             {
                 options.WithFramerate(FrameRate)
-                    .WithConstantRateFactor(21)
+                    .WithConstantRateFactor(18)
+                    .WithVideoBitrate(1800)
+                    .WithVideoCodec("mpeg4")
                     .ForcePixelFormat("yuv420p");
             });
     }

+ 12 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Context;
@@ -10,6 +11,7 @@ public class FuncContext
     public VecD Position { get; private set; }
     public VecI Size { get; private set; }
     public bool HasContext { get; private set; }
+    public RenderingContext RenderingContext { get; set; }
 
     public void ThrowOnMissingContext()
     {
@@ -19,6 +21,16 @@ public class FuncContext
         }
     }
 
+    public FuncContext()
+    {
+        
+    }
+    
+    public FuncContext(RenderingContext renderingContext)
+    {
+        RenderingContext = renderingContext;
+    }
+
     public void UpdateContext(VecD position, VecI size)
     {
         Position = position;

+ 18 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using System.Collections.Concurrent;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -13,8 +14,6 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 public class ModifyImageLeftNode : Node
 {
-    private Pixmap? pixmap;
-
     public InputProperty<Surface?> Image { get; }
     
     public FuncOutputProperty<VecD> Coordinate { get; }
@@ -22,6 +21,8 @@ public class ModifyImageLeftNode : Node
     public FuncOutputProperty<Color> Color { get; }
 
     public override string DisplayName { get; set; } = "MODIFY_IMAGE_LEFT_NODE";
+    
+    private ConcurrentDictionary<RenderingContext, Pixmap> pixmapCache = new();
 
     public ModifyImageLeftNode()
     {
@@ -33,19 +34,29 @@ public class ModifyImageLeftNode : Node
     private Color GetColor(FuncContext context)
     {
         context.ThrowOnMissingContext();
+
+        var targetPixmap = pixmapCache[context.RenderingContext];
         
-        if (pixmap == null)
+        if (targetPixmap == null)
             return new Color();
         
         var x = context.Position.X * context.Size.X;
         var y = context.Position.Y * context.Size.Y;
         
-        return pixmap.GetPixelColor((int)x, (int)y);
+        return targetPixmap.GetPixelColor((int)x, (int)y);
     }
 
-    internal void PreparePixmap()
+    internal void PreparePixmap(RenderingContext forContext)
     {
-        pixmap = Image.Value?.PeekPixels();
+        pixmapCache[forContext] = Image.Value?.DrawingSurface.Snapshot().PeekPixels();
+    }
+    
+    internal void DisposePixmap(RenderingContext forContext)
+    {
+        if (pixmapCache.TryRemove(forContext, out var targetPixmap))
+        {
+            targetPixmap?.Dispose();
+        }
     }
 
     protected override Surface? OnExecute(RenderingContext context)
@@ -53,6 +64,5 @@ public class ModifyImageLeftNode : Node
         return Image.Value;
     }
 
-
     public override Node CreateCopy() => new ModifyImageLeftNode();
 }

+ 46 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -26,6 +26,8 @@ public class ModifyImageRightNode : Node, IPairNodeEnd
 
     public override string DisplayName { get; set; } = "MODIFY_IMAGE_RIGHT_NODE";
 
+    private Surface surface;
+
     public ModifyImageRightNode()
     {
         Coordinate = CreateFuncInput(nameof(Coordinate), "UV", new VecD());
@@ -49,34 +51,60 @@ public class ModifyImageRightNode : Node, IPairNodeEnd
         {
             return null;
         }
-
-        startNode.PreparePixmap();
-
+        
         var width = size.X;
         var height = size.Y;
+        
+        surface = new Surface(size);
+
+        startNode.PreparePixmap(renderingContext);
+        
+        using Pixmap targetPixmap = surface.PeekPixels();
 
-        var surface = new Surface(size);
+        ModifyImageInParallel(renderingContext, targetPixmap, width, height);
+        
+        startNode.DisposePixmap(renderingContext);
+
+        Output.Value = surface;
+
+        return Output.Value;
+    }
 
-        var context = new FuncContext();
+    private unsafe void ModifyImageInParallel(RenderingContext renderingContext, Pixmap targetPixmap, int width, int height)
+    {
+        int threads = Environment.ProcessorCount;
+        int chunkHeight = height / threads;
 
-        for (int y = 0; y < height; y++)
+        Parallel.For(0, threads, i =>
         {
-            for (int x = 0; x < width; x++)
+            FuncContext context = new(renderingContext);
+            
+            int startY = i * chunkHeight;
+            int endY = (i + 1) * chunkHeight;
+            if (i == threads - 1)
             {
-                context.UpdateContext(new VecD((double)x / width, (double)y / height), new VecI(width, height));
-                var uv = Coordinate.Value(context);
-                context.UpdateContext(uv, new VecI(width, height));
-                var color = Color.Value(context);
-                
-                drawingPaint.Color = color;
-
-                surface.DrawingSurface.Canvas.DrawPixel(x, y, drawingPaint);
+                endY = height;
             }
-        }
 
-        Output.Value = surface;
+            Half* drawArray = (Half*)targetPixmap.GetPixels();
 
-        return Output.Value;
+            for (int y = startY; y < endY; y++)
+            {
+                for (int x = 0; x < width; x++)
+                {
+                    context.UpdateContext(new VecD((double)x / width, (double)y / height), new VecI(width, height));
+                    var coordinate = Coordinate.Value(context);
+                    context.UpdateContext(coordinate, new VecI(width, height));
+                    
+                    var color = Color.Value(context);
+                    ulong colorBits = color.ToULong();
+                    
+                    int pixelOffset = (y * width + x) * 4;
+                    Half* drawPixel = drawArray + pixelOffset;
+                    *(ulong*)drawPixel = colorBits;
+                }
+            }
+        });
     }
 
     private void FindStartNode()

+ 7 - 1
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -20,7 +20,7 @@ public class DocumentRenderer
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
         RectI? globalClippingRect = null)
     {
-        using RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
+        RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
         try
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
@@ -60,6 +60,8 @@ public class DocumentRenderer
             }
 
             using var chunkSnapshot = evaluated.DrawingSurface.Snapshot((RectI)sourceRect);
+            
+            if(context.IsDisposed) return new EmptyChunk();
 
             chunk.Surface.DrawingSurface.Canvas.DrawImage(chunkSnapshot, 0, 0, context.ReplacingPaintWithOpacity);
 
@@ -71,6 +73,10 @@ public class DocumentRenderer
         {
             return new EmptyChunk();
         }
+        finally
+        {
+            context.Dispose();
+        }
     }
 
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution,

+ 8 - 0
src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs

@@ -20,6 +20,8 @@ public class RenderingContext : IDisposable
     public ChunkResolution ChunkResolution { get; }
     public VecI DocumentSize { get; set; }
 
+    public bool IsDisposed { get; private set; }
+    
     public RenderingContext(KeyFrameTime frameTime, VecI chunkToUpdate, ChunkResolution chunkResolution, VecI docSize)
     {
         FrameTime = frameTime;
@@ -55,6 +57,12 @@ public class RenderingContext : IDisposable
 
     public void Dispose()
     {
+        if (IsDisposed)
+        {
+            return;
+        }
+        
+        IsDisposed = true;
         BlendModePaint.Dispose();
         BlendModeOpacityPaint.Dispose();
         ReplacingPaintWithOpacity.Dispose();

+ 3 - 1
src/PixiEditor.Desktop/Program.cs

@@ -18,7 +18,9 @@ public class Program
             .UsePlatformDetect()
             .With(new Win32PlatformOptions()
             {
-                RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Vulkan, Win32RenderingMode.AngleEgl }
+                RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Wgl, Win32RenderingMode.AngleEgl },
+                OverlayPopups = true
             })
+           
             .LogToTrace();
 }

+ 3 - 1
src/PixiEditor.DrawingApi.Core/Bridge/IDrawingBackend.cs

@@ -1,4 +1,5 @@
-using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
+using System;
+using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Bridge.Operations;
 
 namespace PixiEditor.DrawingApi.Core.Bridge
@@ -20,5 +21,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge
         public IColorFilterImplementation ColorFilterImplementation { get; }
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
+        public bool IsHardwareAccelerated { get; }
     }
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IBitmapImplementation.cs

@@ -14,4 +14,5 @@ public interface IBitmapImplementation
     public VecI GetSize(IntPtr objectPointer);
     public byte[] GetBytes(IntPtr objectPointer);
     public ImageInfo GetInfo(IntPtr objectPointer);
+    public Pixmap? PeekPixels(IntPtr objectPointer);
 }

+ 2 - 1
src/PixiEditor.DrawingApi.Core/Bridge/Operations/ISurfaceImplementation.cs

@@ -1,5 +1,4 @@
 using System;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
@@ -17,4 +16,6 @@ public interface ISurfaceImplementation
     public DrawingSurface Create(ImageInfo imageInfo);
     public void Dispose(DrawingSurface drawingSurface);
     public object GetNativeSurface(IntPtr objectPointer);
+    public void Flush(DrawingSurface drawingSurface);
 }
+

+ 5 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/Bitmap.cs

@@ -33,4 +33,9 @@ public class Bitmap : NativeObject
     {
         return DrawingBackendApi.Current.BitmapImplementation.FromImage(snapshot.ObjectPointer);
     }
+
+    public Pixmap? PeekPixels()
+    {
+        return DrawingBackendApi.Current.BitmapImplementation.PeekPixels(ObjectPointer);
+    }
 }

+ 5 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/DrawingSurface.cs

@@ -75,5 +75,10 @@ namespace PixiEditor.DrawingApi.Core.Surfaces
         {
             Changed?.Invoke(changedrect);
         }
+
+        public void Flush()
+        {
+            DrawingBackendApi.Current.SurfaceImplementation.Flush(this);
+        }
     }
 }

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/ImageData/ImageInfo.cs

@@ -137,6 +137,8 @@ public struct ImageInfo : System.IEquatable<ImageInfo>
     /// <value />
     public readonly RectI Rect => RectI.Create(this.Width, this.Height);
 
+    public bool GpuBacked { get; set; } = false;
+
 
     public readonly ImageInfo WithSize(VecI size) => this.WithSize(size.X, size.Y);
 

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Surfaces/Pixmap.cs

@@ -41,7 +41,7 @@ public class Pixmap : NativeObject
         DrawingBackendApi.Current.PixmapImplementation.Dispose(ObjectPointer);
     }
 
-    public Color GetPixelColor(int x, int y) => GetPixelColor(new VecI(x, y));
+    public Color GetPixelColor(int x, int y) => GetPixelColor(new VecI(Math.Clamp(x, 0, Width), Math.Clamp(y, 0, Height)));
     
     public Color GetPixelColor(VecI position)
     {

+ 126 - 0
src/PixiEditor.DrawingApi.Core/Texture.cs

@@ -0,0 +1,126 @@
+using System;
+using System.IO;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.DrawingApi.Core;
+
+public class Texture : IDisposable
+{
+    public VecI Size { get; }
+    public DrawingSurface Surface { get; }
+
+    public event SurfaceChangedEventHandler? Changed;
+
+    public bool IsDisposed { get; private set; }
+
+    private bool pixmapUpToDate;
+    private Pixmap pixmap;
+
+    public Texture(VecI size)
+    {
+        Size = size;
+        Surface =
+            DrawingSurface.Create(
+                new ImageInfo(Size.X, Size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgb())
+                {
+                    GpuBacked = true
+                });
+
+        Surface.Changed += SurfaceOnChanged;
+    }
+
+    private void SurfaceOnChanged(RectD? changedRect)
+    {
+        Changed?.Invoke(changedRect);
+    }
+
+
+    public static Texture Load(string path)
+    {
+        if (!File.Exists(path))
+            throw new FileNotFoundException(null, path);
+        using var image = Image.FromEncodedData(path);
+        if (image is null)
+            throw new ArgumentException($"The image with path {path} couldn't be loaded");
+
+        Texture texture = new Texture(image.Size);
+        texture.Surface.Canvas.DrawImage(image, 0, 0);
+
+        return texture;
+    }
+
+    public static Texture Load(byte[] data)
+    {
+        using Image image = Image.FromEncodedData(data);
+        Texture texture = new Texture(image.Size);
+        texture.Surface.Canvas.DrawImage(image, 0, 0);
+
+        return texture;
+    }
+
+    public static Texture? Load(byte[] encoded, ColorType colorType, VecI imageSize)
+    {
+        using var image = Image.FromPixels(new ImageInfo(imageSize.X, imageSize.Y, colorType), encoded);
+        if (image is null)
+            return null;
+
+        var surface = new Texture(new VecI(image.Width, image.Height));
+        surface.Surface.Canvas.DrawImage(image, 0, 0);
+
+        return surface;
+    }
+
+    public Texture CreateResized(VecI newSize, ResizeMethod method)
+    {
+        using Image image = Surface.Snapshot();
+        Texture newTexture = new(newSize);
+        using Paint paint = new();
+
+        FilterQuality filterQuality = method switch
+        {
+            ResizeMethod.HighQuality => FilterQuality.High,
+            ResizeMethod.MediumQuality => FilterQuality.Medium,
+            ResizeMethod.LowQuality => FilterQuality.Low,
+            _ => FilterQuality.None
+        };
+
+        paint.FilterQuality = filterQuality;
+
+        newTexture.Surface.Canvas.DrawImage(image, new RectD(0, 0, newSize.X, newSize.Y), paint);
+
+        return newTexture;
+    }
+
+    public Color? GetSRGBPixel(VecI vecI)
+    {
+        if (vecI.X < 0 || vecI.X >= Size.X || vecI.Y < 0 || vecI.Y >= Size.Y)
+            return null;
+
+        if (!pixmapUpToDate)
+        {
+            pixmapUpToDate = true;
+            pixmap = Surface.PeekPixels();
+        }
+
+        return pixmap.GetPixelColor(vecI);
+    }
+
+    public void AddDirtyRect(RectI dirtyRect)
+    {
+        Changed?.Invoke(new RectD(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
+    }
+
+    public void Dispose()
+    {
+        if (IsDisposed)
+            return;
+
+        IsDisposed = true;
+        Surface.Changed -= SurfaceOnChanged;
+        Surface.Dispose();
+    }
+}

+ 10 - 0
src/PixiEditor.DrawingApi.Skia/Exceptions/GrContextAlreadyInitializedException.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace PixiEditor.DrawingApi.Skia.Exceptions;
+
+public class GrContextAlreadyInitializedException : Exception
+{
+    public GrContextAlreadyInitializedException() : base("GRContext is already initialized")
+    {
+    }
+}

+ 26 - 0
src/PixiEditor.DrawingApi.Skia/Extensions/DrawingBackendExtensions.cs

@@ -0,0 +1,26 @@
+using System;
+using PixiEditor.DrawingApi.Core.Bridge;
+using SkiaSharp;
+
+namespace PixiEditor.DrawingApi.Skia.Extensions;
+
+public static class DrawingBackendExtensions
+{
+    private static RenderGraphicsContext? _renderGraphicsContext;
+    public static IDisposable RenderOnDifferentGrContext(this IDrawingBackend drawingBackend, GRContext targetContext)
+    {
+        if (drawingBackend is not SkiaDrawingBackend skiaDrawingBackend)
+        {
+            throw new InvalidOperationException("This extension method can only be used with SkiaDrawingBackend.");
+        }
+
+        if (_renderGraphicsContext == null)
+        {
+            _renderGraphicsContext = new RenderGraphicsContext(skiaDrawingBackend.GraphicsContext, skiaDrawingBackend.SurfaceImplementation);
+        }
+        
+        _renderGraphicsContext.Target = targetContext;
+        
+        return _renderGraphicsContext;
+    }
+}

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SKObjectImplementation.cs

@@ -17,7 +17,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         
         public T this[IntPtr objPtr]
         {
-            get => ManagedInstances[objPtr];
+            get => ManagedInstances.TryGetValue(objPtr, out var instance) ? instance : throw new ObjectDisposedException(nameof(objPtr));
             set => ManagedInstances[objPtr] = value;
         }
     }

+ 17 - 6
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs

@@ -10,16 +10,20 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
     public class SkiaBitmapImplementation : SkObjectImplementation<SKBitmap>, IBitmapImplementation
     {
         public SkiaImageImplementation ImageImplementation { get; }
-        public SkiaBitmapImplementation(SkiaImageImplementation imgImpl)
+        
+        private readonly SkiaPixmapImplementation _pixmapImplementation;
+
+        public SkiaBitmapImplementation(SkiaImageImplementation imgImpl, SkiaPixmapImplementation pixmapImplementation)
         {
             ImageImplementation = imgImpl;
+            _pixmapImplementation = pixmapImplementation;
         }
 
         public void Dispose(IntPtr objectPointer)
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
-            bitmap.Dispose();   
-            
+            bitmap.Dispose();
+
             ManagedInstances.TryRemove(objectPointer, out _);
         }
 
@@ -37,7 +41,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             ManagedInstances[skBitmap.Handle] = skBitmap;
             return new Bitmap(skBitmap.Handle);
         }
-        
+
         public VecI GetSize(IntPtr objectPointer)
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
@@ -47,15 +51,22 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public byte[] GetBytes(IntPtr objectPointer)
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
-            return bitmap.Bytes; 
+            return bitmap.Bytes;
         }
-        
+
         public ImageInfo GetInfo(IntPtr objectPointer)
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
             return bitmap.Info.ToImageInfo();
         }
 
+        public Pixmap PeekPixels(IntPtr objectPointer)
+        {
+            SKBitmap bitmap = ManagedInstances[objectPointer];
+            SKPixmap pixmap = bitmap.PeekPixels();
+            return _pixmapImplementation.CreateFrom(pixmap);
+        }
+
         public object GetNativeBitmap(IntPtr objectPointer)
         {
             return ManagedInstances[objectPointer];

+ 16 - 2
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs

@@ -57,8 +57,22 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
         public void DrawImage(IntPtr objPtr, Image image, int x, int y, Paint paint)
         {
-            var canvas = ManagedInstances[objPtr];
-            canvas.DrawImage(_imageImpl.ManagedInstances[image.ObjectPointer], x, y, _paintImpl.ManagedInstances[paint.ObjectPointer]);
+            if(!ManagedInstances.TryGetValue(objPtr, out var canvas))
+            {
+                throw new ObjectDisposedException(nameof(canvas));
+            }
+            
+            if (!_paintImpl.ManagedInstances.TryGetValue(paint.ObjectPointer, out var skPaint))
+            {
+                throw new ObjectDisposedException(nameof(paint));
+            }
+            
+            if(!_imageImpl.ManagedInstances.TryGetValue(image.ObjectPointer, out var img))
+            {
+                throw new ObjectDisposedException(nameof(image));
+            }
+            
+            canvas.DrawImage(img, x, y, skPaint);
         }
 
         public int Save(IntPtr objPtr)

+ 79 - 9
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaSurfaceImplementation.cs

@@ -4,6 +4,7 @@ using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
 using SkiaSharp;
 
 namespace PixiEditor.DrawingApi.Skia.Implementations
@@ -14,20 +15,32 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         private readonly SkiaCanvasImplementation _canvasImplementation;
         private readonly SkiaPaintImplementation _paintImplementation;
 
-        public SkiaSurfaceImplementation(SkiaPixmapImplementation pixmapImplementation, SkiaCanvasImplementation canvasImplementation, SkiaPaintImplementation paintImplementation)
+        internal GRContext? GrContext { get; set; }
+
+        public SkiaSurfaceImplementation(GRContext context, SkiaPixmapImplementation pixmapImplementation,
+            SkiaCanvasImplementation canvasImplementation, SkiaPaintImplementation paintImplementation)
         {
             _pixmapImplementation = pixmapImplementation;
             _canvasImplementation = canvasImplementation;
             _paintImplementation = paintImplementation;
+            GrContext = context;
         }
-        
+
         public Pixmap PeekPixels(DrawingSurface drawingSurface)
         {
             SKPixmap pixmap = ManagedInstances[drawingSurface.ObjectPointer].PeekPixels();
+            if (pixmap == null)
+            {
+                using var snapshot = drawingSurface.Snapshot();
+                Bitmap bitmap = Bitmap.FromImage(snapshot);
+                return bitmap.PeekPixels();
+            }
+
             return _pixmapImplementation.CreateFrom(pixmap);
         }
 
-        public bool ReadPixels(DrawingSurface drawingSurface, ImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, int srcX,
+        public bool ReadPixels(DrawingSurface drawingSurface, ImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes,
+            int srcX,
             int srcY)
         {
             return ManagedInstances[drawingSurface.ObjectPointer]
@@ -38,34 +51,85 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
             SKCanvas canvas = _canvasImplementation[surfaceToDraw.ObjectPointer];
             SKPaint paint = _paintImplementation[drawingPaint.ObjectPointer];
-            ManagedInstances[drawingSurface.ObjectPointer].Draw(canvas, x, y, paint);
+            var instance = ManagedInstances[drawingSurface.ObjectPointer];
+            instance.Draw(canvas, x, y, paint);
         }
-        
+
         public DrawingSurface Create(ImageInfo imageInfo, IntPtr pixels, int rowBytes)
         {
-            SKSurface skSurface = SKSurface.Create(imageInfo.ToSkImageInfo(), pixels, rowBytes);
+            SKSurface skSurface = CreateSkiaSurface(imageInfo.ToSkImageInfo(), imageInfo.GpuBacked, pixels, rowBytes);
             return CreateDrawingSurface(skSurface);
         }
 
         public DrawingSurface Create(ImageInfo imageInfo, IntPtr pixelBuffer)
         {
-            SKSurface skSurface = SKSurface.Create(imageInfo.ToSkImageInfo(), pixelBuffer);
+            SKImageInfo info = imageInfo.ToSkImageInfo();
+            SKSurface skSurface = CreateSkiaSurface(info, imageInfo.GpuBacked, pixelBuffer);
             return CreateDrawingSurface(skSurface);
         }
 
+        private SKSurface CreateSkiaSurface(SKImageInfo imageInfo, bool isGpuBacked, IntPtr pixels, int rowBytes)
+        {
+            if (isGpuBacked)
+            {
+                SKSurface skSurface = CreateSkiaSurface(imageInfo, true);
+                using var image = SKImage.FromPixelCopy(imageInfo, pixels, rowBytes);
+
+                var canvas = skSurface.Canvas;
+                canvas.DrawImage(image, new SKPoint(0, 0));
+
+                return skSurface;
+            }
+
+            return SKSurface.Create(imageInfo, pixels, rowBytes);
+        }
+
+        private SKSurface CreateSkiaSurface(SKImageInfo imageInfo, bool isGpuBacked, IntPtr pixels)
+        {
+            if (isGpuBacked)
+            {
+                SKSurface skSurface = CreateSkiaSurface(imageInfo, true);
+                using var image = SKImage.FromPixelCopy(imageInfo, pixels);
+
+                var canvas = skSurface.Canvas;
+                canvas.DrawImage(image, new SKPoint(0, 0));
+
+                return skSurface;
+            }
+
+            return SKSurface.Create(imageInfo, pixels);
+        }
+
         public DrawingSurface Create(Pixmap pixmap)
         {
             SKPixmap skPixmap = _pixmapImplementation[pixmap.ObjectPointer];
-            SKSurface skSurface = SKSurface.Create(skPixmap);
+            var skSurface = CreateSkiaSurface(skPixmap);
+
             return CreateDrawingSurface(skSurface);
         }
 
+        private SKSurface CreateSkiaSurface(SKPixmap skPixmap)
+        {
+            SKSurface skSurface = SKSurface.Create(skPixmap); 
+            return skSurface;
+        }
+
         public DrawingSurface Create(ImageInfo imageInfo)
         {
-            SKSurface skSurface = SKSurface.Create(imageInfo.ToSkImageInfo());
+            SKSurface skSurface = CreateSkiaSurface(imageInfo.ToSkImageInfo(), imageInfo.GpuBacked);
             return CreateDrawingSurface(skSurface);
         }
 
+        private SKSurface CreateSkiaSurface(SKImageInfo info, bool gpu)
+        {
+            if (!gpu || GrContext == null)
+            {
+                return SKSurface.Create(info);
+            }
+
+            return SKSurface.Create(GrContext, false, info);
+        }
+
         public void Dispose(DrawingSurface drawingSurface)
         {
             ManagedInstances[drawingSurface.ObjectPointer].Dispose();
@@ -84,7 +148,13 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
             DrawingSurface surface = new DrawingSurface(skSurface.Handle, canvas);
             ManagedInstances[skSurface.Handle] = skSurface;
+
             return surface;
         }
+
+        public void Flush(DrawingSurface drawingSurface)
+        {
+            ManagedInstances[drawingSurface.ObjectPointer].Flush(true, true);
+        }
     }
 }

+ 39 - 0
src/PixiEditor.DrawingApi.Skia/RenderGraphicsContext.cs

@@ -0,0 +1,39 @@
+using System;
+using PixiEditor.DrawingApi.Skia.Implementations;
+using SkiaSharp;
+
+namespace PixiEditor.DrawingApi.Skia;
+
+public class RenderGraphicsContext : IDisposable
+{
+    private GRContext? _target;
+    public GRContext Original { get; }
+
+    public GRContext Target
+    {
+        get => _target;
+        set
+        {
+            if (_target != null)
+            {
+                throw new InvalidOperationException("Target is already set.");
+            }
+            
+            _target = value;
+            SurfaceImplementation.GrContext = value;
+        }
+    }
+    public SkiaSurfaceImplementation SurfaceImplementation { get; }
+    
+    public RenderGraphicsContext(GRContext context, SkiaSurfaceImplementation surfaceImplementation)
+    {
+        Original = context;
+        SurfaceImplementation = surfaceImplementation;
+    }
+    
+    public void Dispose()
+    {
+        SurfaceImplementation.GrContext = Original;
+        _target = null;
+    }
+}

+ 27 - 10
src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs

@@ -1,12 +1,30 @@
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Bridge.Operations;
+using PixiEditor.DrawingApi.Skia.Exceptions;
 using PixiEditor.DrawingApi.Skia.Implementations;
+using SkiaSharp;
 
 namespace PixiEditor.DrawingApi.Skia
 {
     public class SkiaDrawingBackend : IDrawingBackend
     {
+        public GRContext? GraphicsContext
+        {
+            get => _grContext;
+            set
+            {
+                if (_grContext != null)
+                {
+                    throw new GrContextAlreadyInitializedException();
+                }
+                
+                _grContext = value;
+            }
+        }
+        
+        public bool IsHardwareAccelerated => GraphicsContext != null;
+        
         public IColorImplementation ColorImplementation { get; }
         public IImageImplementation ImageImplementation { get; }
         public IImgDataImplementation ImgDataImplementation { get; }
@@ -15,13 +33,16 @@ namespace PixiEditor.DrawingApi.Skia
         public IVectorPathImplementation PathImplementation { get; }
         public IMatrix3X3Implementation MatrixImplementation { get; }
         public IPixmapImplementation PixmapImplementation { get; }
-        public ISurfaceImplementation SurfaceImplementation { get; }
+        ISurfaceImplementation IDrawingBackend.SurfaceImplementation => SurfaceImplementation;
+        public SkiaSurfaceImplementation SurfaceImplementation { get; }
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
         public IColorFilterImplementation ColorFilterImplementation { get; }
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
 
+        private GRContext _grContext;
+
         public SkiaDrawingBackend()
         {
             ColorImplementation = new SkiaColorImplementation();
@@ -54,26 +75,22 @@ namespace PixiEditor.DrawingApi.Skia
             
             SkiaImageImplementation imgImpl = new SkiaImageImplementation(dataImpl, pixmapImpl);
             ImageImplementation = imgImpl;
-
-            SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl);
+            SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl, pixmapImpl);
             BitmapImplementation = bitmapImpl;
             
             SkiaCanvasImplementation canvasImpl = new SkiaCanvasImplementation(paintImpl, imgImpl, bitmapImpl, pathImpl);
             
-            var surfaceImpl = new SkiaSurfaceImplementation(pixmapImpl, canvasImpl, paintImpl);
+            SurfaceImplementation = new SkiaSurfaceImplementation(GraphicsContext, pixmapImpl, canvasImpl, paintImpl);
 
-            canvasImpl.SetSurfaceImplementation(surfaceImpl);
-            imgImpl.SetSurfaceImplementation(surfaceImpl);
+            canvasImpl.SetSurfaceImplementation(SurfaceImplementation);
+            imgImpl.SetSurfaceImplementation(SurfaceImplementation);
 
             CanvasImplementation = canvasImpl;
-
-            SurfaceImplementation = surfaceImpl;
-
         }
         
         public void Setup()
         {
-            
+            SurfaceImplementation.GrContext = GraphicsContext;
         }
     }
 }

+ 123 - 109
src/PixiEditor.UI.Common/Controls/ProgressBar.axaml

@@ -1,117 +1,131 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:converters="using:Avalonia.Controls.Converters">
-  <Design.PreviewWith>
-    <Border Padding="20">
-      <StackPanel Spacing="10">
-        <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
-        <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
-        <ProgressBar VerticalAlignment="Center" Value="50" />
-        <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
-        <ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" />
-      </StackPanel>
-    </Border>
-  </Design.PreviewWith>
+    <Design.PreviewWith>
+        <Border Padding="20">
+            <StackPanel Spacing="10">
+                <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
+                <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
+                <ProgressBar VerticalAlignment="Center" Value="50" />
+                <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
+                <ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" />
+            </StackPanel>
+        </Border>
+    </Design.PreviewWith>
 
-  <converters:StringFormatConverter x:Key="StringFormatConverter" />
+    <converters:StringFormatConverter x:Key="StringFormatConverter" />
 
-  <ControlTheme x:Key="{x:Type ProgressBar}"
-                TargetType="ProgressBar">
-    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush1}" />
-    <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}" />
-    <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}"/>
-    <Setter Property="Template">
-      <ControlTemplate TargetType="ProgressBar">
-        <Grid>
-          <Border Background="{TemplateBinding Background}"
-                  BorderBrush="{TemplateBinding BorderBrush}"
-                  BorderThickness="{TemplateBinding BorderThickness}"
-                  CornerRadius="{TemplateBinding CornerRadius}">
-            <Panel>
-              <Border Name="PART_Indicator"
-                      Background="{TemplateBinding Foreground}"
-                      CornerRadius="{TemplateBinding CornerRadius}"
-                      IsVisible="{Binding !IsIndeterminate, RelativeSource={RelativeSource TemplatedParent}}" />
-              <Border Name="PART_IndeterminateIndicator"
-                      Background="{TemplateBinding Foreground}"
-                      CornerRadius="{TemplateBinding CornerRadius}"
-                      IsVisible="{Binding IsIndeterminate, RelativeSource={RelativeSource TemplatedParent}}" />
-            </Panel>
-          </Border>
-          <LayoutTransformControl Name="PART_LayoutTransformControl"
-                                  HorizontalAlignment="Center"
-                                  VerticalAlignment="Center"
-                                  IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}">
-            <TextBlock Foreground="{DynamicResource ThemeForegroundBrush}">
-              <TextBlock.Text>
-                <MultiBinding Converter="{StaticResource StringFormatConverter}">
-                  <TemplateBinding Property="ProgressTextFormat" />
-                  <Binding Path="Value"
-                           RelativeSource="{RelativeSource TemplatedParent}" />
-                  <TemplateBinding Property="Percentage" />
-                  <TemplateBinding Property="Minimum" />
-                  <TemplateBinding Property="Maximum" />
-                </MultiBinding>
-              </TextBlock.Text>
-            </TextBlock>
-          </LayoutTransformControl>
-        </Grid>
-      </ControlTemplate>
-    </Setter>
+    <ControlTheme x:Key="{x:Type ProgressBar}"
+                  TargetType="ProgressBar">
+        <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush1}" />
+        <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}" />
+        <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+        <Setter Property="Template">
+            <ControlTemplate TargetType="ProgressBar">
+                <Grid>
+                    <Border Background="{TemplateBinding Background}"
+                            BorderBrush="{TemplateBinding BorderBrush}"
+                            BorderThickness="{TemplateBinding BorderThickness}"
+                            CornerRadius="{TemplateBinding CornerRadius}">
+                        <Panel>
+                            <Border Name="PART_Indicator"
+                                    Background="{TemplateBinding Foreground}"
+                                    CornerRadius="{TemplateBinding CornerRadius}"
+                                    IsVisible="{Binding !IsIndeterminate, RelativeSource={RelativeSource TemplatedParent}}" />
+                            <Border Name="PART_IndeterminateIndicator"
+                                    Background="{TemplateBinding Foreground}"
+                                    CornerRadius="{TemplateBinding CornerRadius}"
+                                    IsVisible="{Binding IsIndeterminate, RelativeSource={RelativeSource TemplatedParent}}" />
+                        </Panel>
+                    </Border>
+                    <LayoutTransformControl Name="PART_LayoutTransformControl"
+                                            HorizontalAlignment="Center"
+                                            VerticalAlignment="Center"
+                                            IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}">
+                        <TextBlock Foreground="{DynamicResource ThemeForegroundBrush}">
+                            <TextBlock.Text>
+                                <MultiBinding Converter="{StaticResource StringFormatConverter}">
+                                    <TemplateBinding Property="ProgressTextFormat" />
+                                    <Binding Path="Value"
+                                             RelativeSource="{RelativeSource TemplatedParent}" />
+                                    <TemplateBinding Property="Percentage" />
+                                    <TemplateBinding Property="Minimum" />
+                                    <TemplateBinding Property="Maximum" />
+                                </MultiBinding>
+                            </TextBlock.Text>
+                        </TextBlock>
+                    </LayoutTransformControl>
+                </Grid>
+            </ControlTemplate>
+        </Setter>
 
-    <Style Selector="^:horizontal /template/ Border#PART_Indicator">
-      <Setter Property="HorizontalAlignment" Value="Left" />
-      <Setter Property="VerticalAlignment" Value="Stretch" />
-    </Style>
-    <Style Selector="^:vertical /template/ Border#PART_Indicator">
-      <Setter Property="HorizontalAlignment" Value="Stretch" />
-      <Setter Property="VerticalAlignment" Value="Bottom" />
-    </Style>
-    <Style Selector="^:horizontal">
-      <Setter Property="MinWidth" Value="200" />
-      <Setter Property="MinHeight" Value="16" />
-    </Style>
-    <Style Selector="^:vertical">
-      <Setter Property="MinWidth" Value="16" />
-      <Setter Property="MinHeight" Value="200" />
-    </Style>
-    <Style Selector="^:vertical /template/ LayoutTransformControl#PART_LayoutTransformControl">
-      <Setter Property="LayoutTransform">
-        <Setter.Value>
-          <RotateTransform Angle="90" />
-        </Setter.Value>
-      </Setter>
-    </Style>
+        <Style Selector="^:horizontal /template/ Border#PART_Indicator">
+            <Setter Property="HorizontalAlignment" Value="Left" />
+            <Setter Property="VerticalAlignment" Value="Stretch" />
+        </Style>
+        <Style Selector="^:vertical /template/ Border#PART_Indicator">
+            <Setter Property="HorizontalAlignment" Value="Stretch" />
+            <Setter Property="VerticalAlignment" Value="Bottom" />
+        </Style>
+        <Style Selector="^:horizontal">
+            <Setter Property="MinWidth" Value="200" />
+            <Setter Property="MinHeight" Value="16" />
+        </Style>
+        <!--<Style Selector="^ /template/ Border#PART_Indicator">
+            <Setter Property="Transitions">
+                <Transitions>
+                    <DoubleTransition Duration="0:0:0.3" Property="Width" />
+                    <DoubleTransition Duration="0:0:0.3" Property="Height" />
+                </Transitions>
+            </Setter>
+        </Style>-->
+        <Style Selector="^:vertical">
+            <Setter Property="MinWidth" Value="16" />
+            <Setter Property="MinHeight" Value="200" />
+        </Style>
+        <Style Selector="^:vertical /template/ LayoutTransformControl#PART_LayoutTransformControl">
+            <Setter Property="LayoutTransform">
+                <Setter.Value>
+                    <RotateTransform Angle="90" />
+                </Setter.Value>
+            </Setter>
+        </Style>
 
-    <Style Selector="^:horizontal:indeterminate /template/ Border#PART_IndeterminateIndicator">
-        <Style.Animations>
-        <Animation Easing="LinearEasing"
-                   IterationCount="Infinite"
-                   Duration="0:0:3">
-          <KeyFrame Cue="0%">
-            <Setter Property="TranslateTransform.X" Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateStartingOffset}" />
-          </KeyFrame>
-          <KeyFrame Cue="100%">
-            <Setter Property="TranslateTransform.X" Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateEndingOffset}" />
-          </KeyFrame>
-        </Animation>
-      </Style.Animations>
-      <Setter Property="Width" Value="{Binding TemplateSettings.ContainerWidth, RelativeSource={RelativeSource TemplatedParent}}" />
-    </Style>
-    <Style Selector="^:vertical:indeterminate /template/ Border#PART_IndeterminateIndicator">
-      <Style.Animations>
-        <Animation Easing="LinearEasing"
-                   IterationCount="Infinite"
-                   Duration="0:0:3">
-          <KeyFrame Cue="0%">
-            <Setter Property="TranslateTransform.Y" Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateStartingOffset}" />
-          </KeyFrame>
-          <KeyFrame Cue="100%">
-            <Setter Property="TranslateTransform.Y" Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateEndingOffset}" />
-          </KeyFrame>
-        </Animation>
-      </Style.Animations>
-      <Setter Property="Height" Value="{Binding TemplateSettings.ContainerWidth, RelativeSource={RelativeSource TemplatedParent}}" />
-    </Style>
-  </ControlTheme>
+        <Style Selector="^:horizontal:indeterminate /template/ Border#PART_IndeterminateIndicator">
+            <Style.Animations>
+                <Animation Easing="LinearEasing"
+                           IterationCount="Infinite"
+                           Duration="0:0:3">
+                    <KeyFrame Cue="0%">
+                        <Setter Property="TranslateTransform.X"
+                                Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateStartingOffset}" />
+                    </KeyFrame>
+                    <KeyFrame Cue="100%">
+                        <Setter Property="TranslateTransform.X"
+                                Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateEndingOffset}" />
+                    </KeyFrame>
+                </Animation>
+            </Style.Animations>
+            <Setter Property="Width"
+                    Value="{Binding TemplateSettings.ContainerWidth, RelativeSource={RelativeSource TemplatedParent}}" />
+        </Style>
+        <Style Selector="^:vertical:indeterminate /template/ Border#PART_IndeterminateIndicator">
+            <Style.Animations>
+                <Animation Easing="LinearEasing"
+                           IterationCount="Infinite"
+                           Duration="0:0:3">
+                    <KeyFrame Cue="0%">
+                        <Setter Property="TranslateTransform.Y"
+                                Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateStartingOffset}" />
+                    </KeyFrame>
+                    <KeyFrame Cue="100%">
+                        <Setter Property="TranslateTransform.Y"
+                                Value="{Binding $parent[ProgressBar].TemplateSettings.IndeterminateEndingOffset}" />
+                    </KeyFrame>
+                </Animation>
+            </Style.Animations>
+            <Setter Property="Height"
+                    Value="{Binding TemplateSettings.ContainerWidth, RelativeSource={RelativeSource TemplatedParent}}" />
+        </Style>
+    </ControlTheme>
 </ResourceDictionary>

+ 8 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -687,5 +687,12 @@
   "LERP_NODE": "Lerp",
   "FROM": "From",
   "TO": "To",
-  "TIME": "Time"
+  "TIME": "Time",
+  "WARMING_UP": "Warming up",
+  "RENDERING_FRAME": "Generating Frame {0}/{1}",
+  "RENDERING_VIDEO": "Rendering Video",
+  "FINISHED": "Finished",
+  "GENERATING_SPRITE_SHEET": "Generating Sprite Sheet",
+  "RENDERING_IMAGE": "Rendering Image",
+  "PROGRESS_POPUP_TITLE": "Progress"
 }

+ 1 - 1
src/PixiEditor/Helpers/MarkupExtensions/EnumExtension.cs

@@ -28,7 +28,7 @@ internal class EnumExtension : MarkupExtension
         }
     }
 
-    public override object ProvideValue(IServiceProvider serviceProvider) // or IXamlServiceProvider for UWP and WinUI
+    public override object ProvideValue(IServiceProvider serviceProvider)
     {
         return Enum.GetValues(EnumType);
     }

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

@@ -345,11 +345,11 @@ internal class DocumentUpdater
     {
         VecI oldSize = doc.SizeBindable;
 
-        foreach ((ChunkResolution res, Surface surf) in doc.Surfaces)
+        foreach ((ChunkResolution res, Texture surf) in doc.Surfaces)
         {
             surf.Dispose();
             VecI size = (VecI)(info.Size * res.Multiplier());
-            doc.Surfaces[res] = new Surface(new VecI(Math.Max(size.X, 1), Math.Max(size.Y, 1))); //TODO: Bgra8888 was here
+            doc.Surfaces[res] = new Texture(new VecI(Math.Max(size.X, 1), Math.Max(size.Y, 1))); //TODO: Bgra8888 was here
         }
 
         doc.SetSize(info.Size);
@@ -358,7 +358,7 @@ internal class DocumentUpdater
 
         VecI documentPreviewSize = StructureHelpers.CalculatePreviewSize(info.Size);
         doc.PreviewSurface.Dispose();
-        doc.PreviewSurface = new Surface(documentPreviewSize); //TODO: Bgra8888 was here
+        doc.PreviewSurface = new Texture(documentPreviewSize); //TODO: Bgra8888 was here
 
         // TODO: Make sure property changed events are raised internally
         // UPDATE: I think I did, but I'll leave it commented out for now

+ 17 - 6
src/PixiEditor/Models/Files/ImageFileType.cs

@@ -4,6 +4,7 @@ using PixiEditor.Helpers;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO.FileEncoders;
 using PixiEditor.Numerics;
@@ -18,17 +19,19 @@ internal abstract class ImageFileType : IoFileType
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Image;
 
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
-        ExportConfig exportConfig)
+        ExportConfig exportConfig, ExportJob? job)
     {
         Surface finalSurface;
         if (exportConfig.ExportAsSpriteSheet)
         {
-            finalSurface = GenerateSpriteSheet(document, exportConfig);
+            job?.Report(0, new LocalizedString("GENERATING_SPRITE_SHEET"));
+            finalSurface = GenerateSpriteSheet(document, exportConfig, job);
             if (finalSurface == null)
                 return SaveResult.UnknownError;
         }
         else
         {
+            job?.Report(0, new LocalizedString("RENDERING_IMAGE")); 
             var maybeBitmap = document.TryRenderWholeImage(0);
             if (maybeBitmap.IsT0)
                 return SaveResult.ConcurrencyError;
@@ -51,11 +54,13 @@ internal abstract class ImageFileType : IoFileType
         UniversalFileEncoder encoder = new(mappedFormat);
         var result = await TrySaveAs(encoder, pathWithExtension, finalSurface);
         finalSurface.Dispose();
+        
+        job?.Report(1, new LocalizedString("FINISHED"));
 
         return result;
     }
 
-    private Surface? GenerateSpriteSheet(DocumentViewModel document, ExportConfig config)
+    private Surface? GenerateSpriteSheet(DocumentViewModel document, ExportConfig config, ExportJob? job)
     {
         if (document is null)
             return null;
@@ -66,21 +71,27 @@ internal abstract class ImageFileType : IoFileType
         columns = Math.Max(1, columns);
 
         Surface surface = new Surface(new VecI(config.ExportSize.X * columns, config.ExportSize.Y * rows));
+        
+        job?.Report(0, new LocalizedString("RENDERING_FRAME", 0, document.AnimationDataViewModel.FramesCount));
 
-        document.RenderFramesProgressive((frame, index) =>
+        document.RenderFramesProgressive(
+            (frame, index) =>
         {
+            job?.CancellationTokenSource.Token.ThrowIfCancellationRequested();
+            
+            job?.Report(index / (double)document.AnimationDataViewModel.FramesCount, new LocalizedString("RENDERING_FRAME", index, document.AnimationDataViewModel.FramesCount));
             int x = index % columns;
             int y = index / columns;
             Surface target = frame;
             if (config.ExportSize != frame.Size)
             {
-               target =
+                target =
                     frame.ResizeNearestNeighbor(new VecI(config.ExportSize.X, config.ExportSize.Y));
             }
             
             surface!.DrawingSurface.Canvas.DrawSurface(target.DrawingSurface, x * config.ExportSize.X, y * config.ExportSize.Y);
             target.Dispose();
-        });
+        }, job?.CancellationTokenSource.Token ?? CancellationToken.None);
 
         return surface;
     }

+ 1 - 1
src/PixiEditor/Models/Files/IoFileType.cs

@@ -45,5 +45,5 @@ internal abstract class IoFileType
         return "*" + extension;
     }
 
-    public abstract Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config);
+    public abstract Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job);
 }

+ 3 - 1
src/PixiEditor/Models/Files/PixiFileType.cs

@@ -16,11 +16,13 @@ internal class PixiFileType : IoFileType
 
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Pixi;
 
-    public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config)
+    public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job)
     {
         try
         {
+            job?.Report(0, "Serializing document");
             await Parser.PixiParser.V5.SerializeAsync(document.ToSerializable(), pathWithExtension);
+            job?.Report(1, "Document serialized");
         }
         catch (UnauthorizedAccessException e)
         {

+ 19 - 3
src/PixiEditor/Models/Files/VideoFileType.cs

@@ -1,5 +1,6 @@
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.IO;
 using PixiEditor.ViewModels.Document;
 
@@ -10,15 +11,23 @@ internal abstract class VideoFileType : IoFileType
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Video;
 
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
-        ExportConfig config)
+        ExportConfig config, ExportJob? job)
     {
         if (config.AnimationRenderer is null)
             return SaveResult.UnknownError;
 
         List<Image> frames = new(); 
+        
+        job?.Report(0, new LocalizedString("WARMING_UP"));
+        
+        int frameRendered = 0;
+        int totalFrames = document.AnimationDataViewModel.FramesCount;
 
         document.RenderFrames(frames, surface =>
         {
+            job?.CancellationTokenSource.Token.ThrowIfCancellationRequested();
+            frameRendered++;
+            job?.Report(((double)frameRendered / totalFrames) / 2, new LocalizedString("RENDERING_FRAME", frameRendered, totalFrames));
             if (config.ExportSize != surface.Size)
             {
                 return surface.ResizeNearestNeighbor(config.ExportSize);
@@ -26,8 +35,15 @@ internal abstract class VideoFileType : IoFileType
 
             return surface;
         });
-
-        var result = await config.AnimationRenderer.RenderAsync(frames, pathWithExtension);
+        
+        job?.Report(0.5, new LocalizedString("RENDERING_VIDEO"));
+        CancellationToken token = job?.CancellationTokenSource.Token ?? CancellationToken.None;
+        var result = await config.AnimationRenderer.RenderAsync(frames, pathWithExtension, token, progress =>
+        {
+            job?.Report((progress / 100f) * 0.5f + 0.5, new LocalizedString("RENDERING_VIDEO"));
+        });
+        
+        job?.Report(1, new LocalizedString("FINISHED"));
         
         foreach (var frame in frames)
         {

+ 2 - 2
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -25,9 +25,9 @@ internal interface IDocument : IHandler
     public IAnimationHandler AnimationHandler { get; }
     public VectorPath SelectionPathBindable { get; }
     public INodeGraphHandler NodeGraphHandler { get; }
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; }
+    public Dictionary<ChunkResolution, Texture> Surfaces { get; set; }
     public DocumentStructureModule StructureHelper { get; }
-    public Surface PreviewSurface { get; set; }
+    public Texture PreviewSurface { get; set; }
     public bool AllChangesSaved { get; }
     public string CoordinatesString { get; set; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }

+ 1 - 1
src/PixiEditor/Models/Handlers/IKeyFrameHandler.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.Models.Handlers;
 
 internal interface IKeyFrameHandler
 {
-    public Surface? PreviewSurface { get; set; }
+    public Texture? PreviewSurface { get; set; }
     public int StartFrameBindable { get; }
     public int DurationBindable { get; }
     public bool IsSelected { get; set; }

+ 1 - 1
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -15,7 +15,7 @@ public interface INodeHandler : INotifyPropertyChanged
     public string InternalName { get; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
-    public Surface ResultPreview { get; set; }
+    public Texture? ResultPreview { get; set; }
     public VecD PositionBindable { get; set; }
     public bool IsSelected { get; set; }
     public void TraverseBackwards(Func<INodeHandler, bool> func);

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

@@ -11,7 +11,7 @@ namespace PixiEditor.Models.Handlers;
 
 public interface IReferenceLayerHandler : IHandler
 {
-    public Surface? ReferenceBitmap { get; }
+    public Texture? ReferenceBitmap { get; }
     public ShapeCorners ReferenceShapeBindable { get; set; }
     public bool IsTopMost { get; set; }
     public bool IsTransforming { get; set; }

+ 2 - 2
src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs

@@ -12,8 +12,8 @@ namespace PixiEditor.Models.Handlers;
 internal interface IStructureMemberHandler : INodeHandler
 {
     public bool HasMaskBindable { get; }
-    public Surface? MaskPreviewSurface { get; set; }
-    public Surface? PreviewSurface { get; set; }
+    public Texture? MaskPreviewSurface { get; set; }
+    public Texture? PreviewSurface { get; set; }
     public bool MaskIsVisibleBindable { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public float OpacityBindable { get; set; }

+ 29 - 0
src/PixiEditor/Models/IO/ExportJob.cs

@@ -0,0 +1,29 @@
+namespace PixiEditor.Models.IO;
+
+public class ExportJob
+{
+    public int Progress { get; private set; }
+    public string Status { get; private set; }
+    public CancellationTokenSource CancellationTokenSource { get; set; }
+    
+    public event Action<int, string> ProgressChanged;
+    public event Action Finished;
+    public event Action Cancelled;
+    
+    public ExportJob()
+    {
+        CancellationTokenSource = new CancellationTokenSource();
+    }
+    
+    public void Finish()
+    {
+        Finished?.Invoke();
+    }
+    
+    public void Report(double progress, string status)
+    {
+        Progress = (int)Math.Clamp(Math.Round(progress * 100), 0, 100);
+        Status = status;
+        ProgressChanged?.Invoke(Progress, Status);
+    }
+}

+ 8 - 6
src/PixiEditor/Models/IO/Exporter.cs

@@ -51,7 +51,7 @@ internal class Exporter
     /// <summary>
     /// Attempts to save file using a SaveFileDialog
     /// </summary>
-    public static async Task<ExporterResult> TrySaveWithDialog(DocumentViewModel document, ExportConfig exportConfig)
+    public static async Task<ExporterResult> TrySaveWithDialog(DocumentViewModel document, ExportConfig exportConfig, ExportJob? job)
     {
         ExporterResult result = new(DialogSaveResult.UnknownError, null);
 
@@ -70,7 +70,7 @@ internal class Exporter
 
             var fileType = SupportedFilesHelper.GetSaveFileType(FileTypeDialogDataSet.SetKind.Any, file);
 
-            (SaveResult Result, string finalPath) saveResult = await TrySaveUsingDataFromDialog(document, file.Path.LocalPath, fileType, exportConfig);
+            (SaveResult Result, string finalPath) saveResult = await TrySaveUsingDataFromDialog(document, file.Path.LocalPath, fileType, exportConfig, job);
             if (saveResult.Result == SaveResult.Success)
             {
                 result.Path = saveResult.finalPath;
@@ -85,10 +85,10 @@ internal class Exporter
     /// <summary>
     /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// </summary>
-    public static async Task<(SaveResult result, string finalPath)> TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, IoFileType fileTypeFromDialog, ExportConfig exportConfig)
+    public static async Task<(SaveResult result, string finalPath)> TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, IoFileType fileTypeFromDialog, ExportConfig exportConfig, ExportJob? job)
     {
         string finalPath = SupportedFilesHelper.FixFileExtension(pathFromDialog, fileTypeFromDialog);
-        var saveResult = await TrySaveAsync(document, finalPath, exportConfig);
+        var saveResult = await TrySaveAsync(document, finalPath, exportConfig, job);
         if (saveResult != SaveResult.Success)
             finalPath = "";
 
@@ -98,7 +98,7 @@ internal class Exporter
     /// <summary>
     /// Attempts to save the document into the given location, filetype is inferred from path
     /// </summary>
-    public static async Task<SaveResult> TrySaveAsync(DocumentViewModel document, string pathWithExtension, ExportConfig exportConfig)
+    public static async Task<SaveResult> TrySaveAsync(DocumentViewModel document, string pathWithExtension, ExportConfig exportConfig, ExportJob? job)
     {
         string directory = Path.GetDirectoryName(pathWithExtension);
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
@@ -109,7 +109,9 @@ internal class Exporter
         if (typeFromPath is null)
             return SaveResult.UnknownError;
         
-        return await typeFromPath.TrySave(pathWithExtension, document, exportConfig);
+        var result = await typeFromPath.TrySave(pathWithExtension, document, exportConfig, job);
+        job?.Finish();
+        return result;
     }
 
     public static void SaveAsGZippedBytes(string path, Surface surface)

+ 26 - 8
src/PixiEditor/Models/IO/Importer.cs

@@ -33,9 +33,9 @@ internal class Importer : ObservableObject
     /// <returns>WriteableBitmap of imported image.</returns>
     public static Surface? ImportImage(string path, VecI size)
     {
-        if (!Path.Exists(path)) 
+        if (!Path.Exists(path))
             throw new MissingFileException();
-        
+
         Surface original;
         try
         {
@@ -45,7 +45,7 @@ internal class Importer : ObservableObject
         {
             throw new CorruptedFileException(e);
         }
-        
+
         if (original.Size == size || size == VecI.NegativeOne)
         {
             return original;
@@ -64,7 +64,8 @@ internal class Importer : ObservableObject
         }
         catch (NotSupportedException e)
         {
-            throw new InvalidFileTypeException(new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED", Path.GetExtension(path)), e);
+            throw new InvalidFileTypeException(
+                new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED", Path.GetExtension(path)), e);
         }
         /*catch (FileFormatException e) TODO: Not found in Avalonia
         {
@@ -126,7 +127,7 @@ internal class Importer : ObservableObject
                 // TODO: Handle
                 throw new RecoverableException();
             }
-            
+
             var pixiDocument = parser.Deserialize(file);
 
             var document = pixiDocument switch
@@ -150,13 +151,30 @@ internal class Importer : ObservableObject
         }
     }
 
-    public static Surface GetPreviewBitmap(string path)
+    public static Texture GetPreviewTexture(string path)
     {
         if (!IsSupportedFile(path))
         {
-            throw new InvalidFileTypeException(new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED", Path.GetExtension(path)));
+            throw new InvalidFileTypeException(new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED",
+                Path.GetExtension(path)));
         }
-        
+
+        if (Path.GetExtension(path) != ".pixi")
+            return Texture.Load(path);
+
+        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
+
+        return Texture.Load(PixiParser.ReadPreview(fileStream));
+    }
+
+    public static Surface GetPreviewSurface(string path)
+    {
+        if (!IsSupportedFile(path))
+        {
+            throw new InvalidFileTypeException(new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED",
+                Path.GetExtension(path)));
+        }
+
         if (Path.GetExtension(path) != ".pixi")
             return Surface.Load(path);
 

+ 39 - 20
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Threading.Tasks;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
@@ -196,7 +197,7 @@ internal class CanvasUpdater
                 globalScaledClippingRectangle =
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
 
-            Surface screenSurface = doc.Surfaces[resolution];
+            Texture screenSurface = doc.Surfaces[resolution];
             foreach (var chunkPos in chunks)
             {
                 RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle,
@@ -214,38 +215,56 @@ internal class CanvasUpdater
         }
     }
 
-    private void RenderChunk(VecI chunkPos, Surface screenSurface, ChunkResolution resolution,
+    private void RenderChunk(VecI chunkPos, Texture screenSurface, ChunkResolution resolution,
         RectI? globalClippingRectangle, RectI? globalScaledClippingRectangle)
     {
         if (screenSurface is null || screenSurface.IsDisposed)
             return;
 
-        if (globalScaledClippingRectangle is not null)
-        {
-            screenSurface.DrawingSurface.Canvas.Save();
-            screenSurface.DrawingSurface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
-        }
 
         doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime, globalClippingRectangle)
             .Switch(
                 (Chunk chunk) =>
                 {
-                    if (screenSurface.IsDisposed) return;
-                    
-                    screenSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface,
-                        chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
-                    chunk.Dispose();
+                    Dispatcher.UIThread.Post(() =>
+                    {
+                        if (screenSurface.IsDisposed) return;
+
+                        if (globalScaledClippingRectangle is not null)
+                        {
+                            screenSurface.Surface.Canvas.Save();
+                            screenSurface.Surface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
+                        }
+
+                        screenSurface.Surface.Canvas.DrawSurface(
+                            chunk.Surface.DrawingSurface,
+                            chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
+                        chunk.Dispose();
+
+
+                        if (globalScaledClippingRectangle is not null)
+                            screenSurface.Surface.Canvas.Restore();
+                    });
                 },
                 (EmptyChunk _) =>
                 {
-                    if (screenSurface.IsDisposed) return;
-                    
-                    var pos = chunkPos * resolution.PixelSize();
-                    screenSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
-                        resolution.PixelSize(), ClearPaint);
-                });
+                    Dispatcher.UIThread.Post(() =>
+                    {
+                        if (screenSurface.IsDisposed) return;
 
-        if (globalScaledClippingRectangle is not null)
-            screenSurface.DrawingSurface.Canvas.Restore();
+                        if (globalScaledClippingRectangle is not null)
+                        {
+                            screenSurface.Surface.Canvas.Save();
+                            screenSurface.Surface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
+                        }
+
+                        var pos = chunkPos * resolution.PixelSize();
+                        screenSurface.Surface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
+                            resolution.PixelSize(), ClearPaint);
+                        
+                        if (globalScaledClippingRectangle is not null)
+                            screenSurface.Surface.Canvas.Restore();
+                    });
+                });
     }
 }

+ 128 - 103
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -4,6 +4,7 @@ using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
@@ -12,6 +13,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
@@ -191,7 +193,7 @@ internal class MemberPreviewUpdater
             if (member is null)
                 continue;
 
-            if (forMasks && member.Mask.Value is null)
+            if (forMasks && member.Mask.NonOverridenValue is null)
             {
                 newPreviewBitmapSizes.Add(guid, null);
                 continue;
@@ -237,17 +239,14 @@ internal class MemberPreviewUpdater
                 if (member.PreviewSurface is not null && member.PreviewSurface.Size.X == newSize.Value.previewSize.X &&
                     member.PreviewSurface.Size.Y == newSize.Value.previewSize.Y)
                 {
-                    member.PreviewSurface!.DrawingSurface.Canvas.Clear();
+                    member.PreviewSurface!.Surface.Canvas.Clear();
                 }
                 else
                 {
                     member.PreviewSurface?.Dispose();
-                    member.PreviewSurface = new Surface(newSize.Value.previewSize);
+                    member.PreviewSurface = new Texture(newSize.Value.previewSize);
                 }
             }
-
-            //TODO: Make sure PreviewBitmap implementation raises PropertyChanged
-            //member.OnPropertyChanged(nameof(member.PreviewBitmap));
         }
 
         // update masks
@@ -262,11 +261,8 @@ internal class MemberPreviewUpdater
             }
             else
             {
-                member.MaskPreviewSurface = new Surface(newSize.Value.previewSize); // TODO: premul bgra8888 was here
+                member.MaskPreviewSurface = new Texture(newSize.Value.previewSize); // TODO: premul bgra8888 was here
             }
-
-            //TODO: Make sure MaskPreviewBitmap implementation raises PropertyChanged
-            //member.OnPropertyChanged(nameof(member.MaskPreviewBitmap));
         }
     }
 
@@ -278,7 +274,7 @@ internal class MemberPreviewUpdater
     private RectI? GetOrFindMemberTightBounds(IReadOnlyStructureNode member, int atFrame,
         AffectedArea currentlyAffectedArea, bool forMask)
     {
-        if (forMask && member.Mask.Value is null)
+        if (forMask && member.Mask.NonOverridenValue is null)
             throw new InvalidOperationException();
 
         RectI? prevTightBounds = null;
@@ -308,10 +304,10 @@ internal class MemberPreviewUpdater
     /// </summary>
     private RectI? FindLayerTightBounds(IReadOnlyLayerNode layer, int frame, bool forMask)
     {
-        if (layer.Mask.Value is null && forMask)
+        if (layer.Mask.NonOverridenValue is null && forMask)
             throw new InvalidOperationException();
 
-        if (layer.Mask.Value is not null && forMask)
+        if (layer.Mask.NonOverridenValue is not null && forMask)
             return FindImageTightBoundsFast(layer.Mask.Value);
 
         if (layer is IReadOnlyImageNode raster)
@@ -440,22 +436,22 @@ internal class MemberPreviewUpdater
             };
             var pos = chunkPos * resolution.PixelSize();
             var rendered = doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime);
-            doc.PreviewSurface.DrawingSurface.Canvas.Save();
-            doc.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-            doc.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
-            doc.PreviewSurface.DrawingSurface.Canvas.Scale(1 / (float)resolution.Multiplier());
+            doc.PreviewSurface.Surface.Canvas.Save();
+            doc.PreviewSurface.Surface.Canvas.Scale(scaling);
+            doc.PreviewSurface.Surface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
+            doc.PreviewSurface.Surface.Canvas.Scale(1 / (float)resolution.Multiplier());
             if (rendered.IsT1)
             {
-                doc.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
+                doc.PreviewSurface.Surface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
                     resolution.PixelSize(), ClearPaint);
             }
             else if (rendered.IsT0)
             {
                 using var renderedChunk = rendered.AsT0;
-                renderedChunk.DrawChunkOn(doc.PreviewSurface.DrawingSurface, pos, SmoothReplacingPaint);
+                renderedChunk.DrawChunkOn(doc.PreviewSurface.Surface, pos, SmoothReplacingPaint);
             }
 
-            doc.PreviewSurface.DrawingSurface.Canvas.Restore();
+            doc.PreviewSurface.Surface.Canvas.Restore();
         }
 
         if (somethingChanged)
@@ -543,14 +539,14 @@ internal class MemberPreviewUpdater
         IReadOnlyStructureNode member, [DisallowNull] AffectedArea? affArea, VecI position, float scaling)
     {
         bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
-        if(!isEditingRootImage)
+        if (!isEditingRootImage)
             return;
-        
+
         if (keyFrame.PreviewSurface == null ||
             keyFrame.PreviewSurface.Size != memberVM.PreviewSurface.Size)
         {
             keyFrame.PreviewSurface?.Dispose();
-            keyFrame.PreviewSurface = new Surface(memberVM.PreviewSurface.Size);
+            keyFrame.PreviewSurface = new Texture(memberVM.PreviewSurface.Size);
         }
 
         RenderLayerMainPreview((IReadOnlyLayerNode)member, keyFrame.PreviewSurface, affArea.Value,
@@ -564,73 +560,79 @@ internal class MemberPreviewUpdater
         AffectedArea area,
         VecI position, float scaling)
     {
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Save();
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Translate(-position);
-        memberVM.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
-        foreach (var chunk in area.Chunks)
+        QueueRender(() =>
         {
-            var pos = chunk * ChunkResolution.Full.PixelSize();
-            // drawing in full res here is kinda slow
-            // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
-            var contentNode = folder.Content.Connection?.Node;
+            memberVM.PreviewSurface.Surface.Canvas.Save();
+            memberVM.PreviewSurface.Surface.Canvas.Scale(scaling);
+            memberVM.PreviewSurface.Surface.Canvas.Translate(-position);
+            memberVM.PreviewSurface.Surface.Canvas.ClipRect((RectD)area.GlobalArea);
+            foreach (var chunk in area.Chunks)
+            {
+                var pos = chunk * ChunkResolution.Full.PixelSize();
+                // drawing in full res here is kinda slow
+                // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
+                var contentNode = folder.Content.Connection?.Node;
 
-            OneOf<Chunk, EmptyChunk> rendered;
+                OneOf<Chunk, EmptyChunk> rendered;
 
-            if (contentNode is null)
-            {
-                rendered = new EmptyChunk();
-            }
-            else
-            {
-                rendered = doc.Renderer.RenderChunk(chunk, ChunkResolution.Full, contentNode,
-                    doc.AnimationHandler.ActiveFrameBindable);
-            }
+                if (contentNode is null)
+                {
+                    rendered = new EmptyChunk();
+                }
+                else
+                {
+                    rendered = doc.Renderer.RenderChunk(chunk, ChunkResolution.Full, contentNode,
+                        doc.AnimationHandler.ActiveFrameBindable);
+                }
 
-            if (rendered.IsT0)
-            {
-                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos,
-                    scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
-                rendered.AsT0.Dispose();
-            }
-            else
-            {
-                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(),
-                    ChunkResolution.Full.PixelSize(), ClearPaint);
+                if (rendered.IsT0)
+                {
+                    memberVM.PreviewSurface.Surface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos,
+                        scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
+                    rendered.AsT0.Dispose();
+                }
+                else
+                {
+                    memberVM.PreviewSurface.Surface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(),
+                        ChunkResolution.Full.PixelSize(), ClearPaint);
+                }
             }
-        }
 
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
+            memberVM.PreviewSurface.Surface.Canvas.Restore();
+        });
     }
 
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
     /// </summary>
-    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Surface surface, AffectedArea area,
+    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Texture surface, AffectedArea area,
         VecI position, float scaling, int frame)
     {
-        surface.DrawingSurface.Canvas.Save();
-        surface.DrawingSurface.Canvas.Scale(scaling);
-        surface.DrawingSurface.Canvas.Translate(-position);
-        surface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
-
-        foreach (var chunk in area.Chunks)
+        QueueRender(() =>
         {
-            var pos = chunk * ChunkResolution.Full.PixelSize();
-            if (layer is not IReadOnlyImageNode raster) return;
-            IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(frame);
-
-            if (!result.DrawCommittedChunkOn(
-                    chunk,
-                    ChunkResolution.Full, surface.DrawingSurface, pos,
-                    scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
+            surface.Surface.Canvas.Save();
+            surface.Surface.Canvas.Scale(scaling);
+            surface.Surface.Canvas.Translate(-position);
+            surface.Surface.Canvas.ClipRect((RectD)area.GlobalArea);
+
+            foreach (var chunk in area.Chunks)
             {
-                surface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
-                    ChunkyImage.FullChunkSize, ClearPaint);
+                var pos = chunk * ChunkResolution.Full.PixelSize();
+                if (layer is not IReadOnlyImageNode raster) return;
+                IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(frame);
+
+                if (!result.DrawCommittedChunkOn(
+                        chunk,
+                        ChunkResolution.Full, surface.Surface, pos,
+                        scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
+                {
+                    surface.Surface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
+                        ChunkyImage.FullChunkSize, ClearPaint);
+                }
             }
-        }
 
-        surface.DrawingSurface.Canvas.Restore();
+            surface.Surface.Canvas.Restore();
+        });
     }
 
     private void RenderAnimationFramePreview(IReadOnlyImageNode node, IKeyFrameHandler keyFrameVM, AffectedArea area)
@@ -638,24 +640,27 @@ internal class MemberPreviewUpdater
         if (keyFrameVM.PreviewSurface is null)
         {
             keyFrameVM.PreviewSurface =
-                new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
+                new Texture(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
         }
 
-        keyFrameVM.PreviewSurface!.DrawingSurface.Canvas.Save();
-        float scaling = (float)keyFrameVM.PreviewSurface.Size.X / internals.Tracker.Document.Size.X;
-        keyFrameVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-        foreach (var chunk in area.Chunks)
+        QueueRender(() =>
         {
-            var pos = chunk * ChunkResolution.Full.PixelSize();
-            if (!node.GetLayerImageByKeyFrameGuid(keyFrameVM.Id).DrawCommittedChunkOn(chunk, ChunkResolution.Full,
-                    keyFrameVM.PreviewSurface!.DrawingSurface, pos, ReplacingPaint))
+            keyFrameVM.PreviewSurface!.Surface.Canvas.Save();
+            float scaling = (float)keyFrameVM.PreviewSurface.Size.X / internals.Tracker.Document.Size.X;
+            keyFrameVM.PreviewSurface.Surface.Canvas.Scale(scaling);
+            foreach (var chunk in area.Chunks)
             {
-                keyFrameVM.PreviewSurface!.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
-                    ChunkyImage.FullChunkSize, ClearPaint);
+                var pos = chunk * ChunkResolution.Full.PixelSize();
+                if (!node.GetLayerImageByKeyFrameGuid(keyFrameVM.Id).DrawCommittedChunkOn(chunk, ChunkResolution.Full,
+                        keyFrameVM.PreviewSurface!.Surface, pos, ReplacingPaint))
+                {
+                    keyFrameVM.PreviewSurface!.Surface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
+                        ChunkyImage.FullChunkSize, ClearPaint);
+                }
             }
-        }
 
-        keyFrameVM.PreviewSurface!.DrawingSurface.Canvas.Restore();
+            keyFrameVM.PreviewSurface!.Surface.Canvas.Restore();
+        });
     }
 
     private void RenderMaskPreviews(
@@ -701,19 +706,23 @@ internal class MemberPreviewUpdater
 
             var member = internals.Tracker.Document.FindMemberOrThrow(guid);
 
-            memberVM.MaskPreviewSurface!.DrawingSurface.Canvas.Save();
-            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Translate(-position);
-            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)affArea.Value.GlobalArea);
-            foreach (var chunk in affArea.Value.Chunks)
+            QueueRender(() =>
             {
-                var pos = chunk * ChunkResolution.Full.PixelSize();
-                member.Mask!.Value.DrawMostUpToDateChunkOn
-                (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface.DrawingSurface, pos,
-                    scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
-            }
+                memberVM.MaskPreviewSurface!.Surface.Canvas.Save();
+                memberVM.MaskPreviewSurface.Surface.Canvas.Scale(scaling);
+                memberVM.MaskPreviewSurface.Surface.Canvas.Translate(-position);
+                memberVM.MaskPreviewSurface.Surface.Canvas.ClipRect((RectD)affArea.Value.GlobalArea);
+                foreach (var chunk in affArea.Value.Chunks)
+                {
+                    var pos = chunk * ChunkResolution.Full.PixelSize();
+                    member.Mask!.Value.DrawMostUpToDateChunkOn
+                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface.Surface, pos,
+                        scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
+                }
+
+                memberVM.MaskPreviewSurface.Surface.Canvas.Restore();
+            });
 
-            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Restore();
             infos.Add(new MaskPreviewDirty_RenderInfo(guid));
         }
     }
@@ -739,22 +748,38 @@ internal class MemberPreviewUpdater
             if (nodeVm.ResultPreview == null)
             {
                 nodeVm.ResultPreview =
-                    new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size, 150));
+                    new Texture(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size, 150));
             }
 
             float scalingX = (float)nodeVm.ResultPreview.Size.X / node.CachedResult.Size.X;
             float scalingY = (float)nodeVm.ResultPreview.Size.Y / node.CachedResult.Size.Y;
 
-            nodeVm.ResultPreview.DrawingSurface.Canvas.Save();
-            nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
+            QueueRender(() =>
+            {
+                nodeVm.ResultPreview.Surface.Canvas.Save();
+                nodeVm.ResultPreview.Surface.Canvas.Scale(scalingX, scalingY);
+
+                RectI region = new RectI(0, 0, node.CachedResult.Size.X, node.CachedResult.Size.Y);
 
-            RectI region = new RectI(0, 0, node.CachedResult.Size.X, node.CachedResult.Size.Y);
+                nodeVm.ResultPreview.Surface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0,
+                    ReplacingPaint);
 
-            nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0,
-                ReplacingPaint);
+                nodeVm.ResultPreview.Surface.Canvas.Restore();
+            });
 
-            nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
             infos.Add(new NodePreviewDirty_RenderInfo(node.Id));
         }
     }
+
+    private void QueueRender(Action action)
+    {
+        if (!DrawingBackendApi.Current.IsHardwareAccelerated)
+        {
+            action();
+        }
+        else
+        {
+            Dispatcher.UIThread.Post(action, DispatcherPriority.Render);
+        }
+    }
 }

+ 8 - 8
src/PixiEditor/Models/UserData/RecentlyOpenedDocument.cs

@@ -20,7 +20,7 @@ internal class RecentlyOpenedDocument : ObservableObject
 
     private string filePath;
 
-    private Surface previewBitmap;
+    private Texture previewBitmap;
 
     public string FilePath
     {
@@ -61,7 +61,7 @@ internal class RecentlyOpenedDocument : ObservableObject
         }
     }
 
-    public Surface PreviewBitmap
+    public Texture PreviewBitmap
     {
         get
         {
@@ -80,7 +80,7 @@ internal class RecentlyOpenedDocument : ObservableObject
         FilePath = path;
     }
 
-    private Surface? LoadPreviewBitmap()
+    private Texture? LoadPreviewBitmap()
     {
         if (!File.Exists(FilePath))
         {
@@ -91,7 +91,7 @@ internal class RecentlyOpenedDocument : ObservableObject
         {
             try
             {
-                return Importer.GetPreviewBitmap(FilePath);
+                return Importer.GetPreviewTexture(FilePath);
             }
             catch
             {
@@ -101,11 +101,11 @@ internal class RecentlyOpenedDocument : ObservableObject
 
         if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
         {
-            Surface bitmap = null;
+            Texture bitmap = null;
 
             try
             {
-                bitmap = Surface.Load(FilePath);
+                bitmap = Texture.Load(FilePath);
             }
             catch (RecoverableException)
             {
@@ -122,12 +122,12 @@ internal class RecentlyOpenedDocument : ObservableObject
         return null;
     }
 
-    private Surface DownscaleToMaxSize(Surface bitmap)
+    private Texture DownscaleToMaxSize(Texture bitmap)
     {
         if (bitmap.Size.X > Constants.MaxPreviewWidth || bitmap.Size.Y > Constants.MaxPreviewHeight)
         {
             double factor = Math.Min(Constants.MaxPreviewWidth / (double)bitmap.Size.X, Constants.MaxPreviewHeight / (double)bitmap.Size.Y);
-            var scaledBitmap = bitmap.Resize(new VecI((int)(bitmap.Size.X * factor), (int)(bitmap.Size.Y * factor)),
+            var scaledBitmap = bitmap.CreateResized(new VecI((int)(bitmap.Size.X * factor), (int)(bitmap.Size.Y * factor)),
                 ResizeMethod.HighQuality);
             return scaledBitmap;
         }

+ 24 - 19
src/PixiEditor/Styles/Templates/KeyFrame.axaml

@@ -5,24 +5,26 @@
                     xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
-        <Setter Property="ClipToBounds" Value="False"/>
-        <Setter Property="Height" Value="70"/>
-        <Setter Property="MinWidth" Value="35"/>
+        <Setter Property="ClipToBounds" Value="False" />
+        <Setter Property="Height" Value="70" />
+        <Setter Property="MinWidth" Value="35" />
         <Setter Property="Template">
             <ControlTemplate>
                 <Grid>
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
-                            Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 5"
+                            Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 15"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                         <Grid>
-                            <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                            <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5"
+                                   Cursor="SizeWestEast" Background="Transparent" ZIndex="1" />
                             <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft"
-                                   Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                                   Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1" />
                         </Grid>
                     </Border>
-                    
+
                     <Border IsVisible="{Binding !IsCollapsed, RelativeSource={RelativeSource TemplatedParent}}"
-                        CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60" Margin="-30, 0, 0, 0"
+                            CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60"
+                            Margin="-30, 0, 0, 0"
                             BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True" Name="PreviewBorder"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             RenderOptions.BitmapInterpolationMode="None">
@@ -33,8 +35,8 @@
                                 </ImageBrush.Transform>
                             </ImageBrush>
                         </Border.Background>
-                        <visuals:SurfaceControl
-                            Surface="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
+                        <visuals:TextureControl
+                            Texture="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
                             Stretch="Uniform" Width="60" Height="60">
                             <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                 <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
@@ -43,29 +45,32 @@
                                     <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
                                 </MultiBinding>
                             </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                        </visuals:SurfaceControl>
+                        </visuals:TextureControl>
                     </Border>
                 </Grid>
             </ControlTemplate>
         </Setter>
-        
+
         <Style Selector="^:collapsed">
-            <Setter Property="Height" Value="30"/>
-            <Setter Property="MinWidth" Value="5"/>
+            <Setter Property="Height" Value="30" />
+            <Setter Property="MinWidth" Value="5" />
         </Style>
-        
+
         <Style Selector="^:collapsed /template/ Panel#PART_ResizePanelLeft">
-            <Setter Property="Margin" Value="0"/>
+            <Setter Property="Margin" Value="0" />
+        </Style>
+        <Style Selector="^:collapsed /template/ Border#MainBorder">
+            <Setter Property="Margin" Value="0 5" />
         </Style>
-        
+
         <Style Selector="^:selected /template/ Border#MainBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>
-        
+
         <Style Selector="^:selected /template/ Border#PreviewBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>
-        
+
         <Style Selector="^:disabled">
             <Setter Property="Opacity" Value="0.5" />
         </Style>

+ 5 - 5
src/PixiEditor/Styles/Templates/NodeView.axaml

@@ -55,14 +55,14 @@
                             </Border>
                             <Border IsVisible="{Binding !!ResultPreview, RelativeSource={RelativeSource TemplatedParent}}"
                                 CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
-                                <visuals:SurfaceControl Width="200" Height="200"
-                                                        Surface="{TemplateBinding ResultPreview}"
+                                <visuals:TextureControl Width="200" Height="200"
+                                                        Texture="{TemplateBinding ResultPreview}"
                                                         RenderOptions.BitmapInterpolationMode="None">
-                                    <visuals:SurfaceControl.Background>
+                                    <visuals:TextureControl.Background>
                                         <ImageBrush Source="/Images/CheckerTile.png"
                                                     TileMode="Tile" DestinationRect="0, 0, 25, 25" />
-                                    </visuals:SurfaceControl.Background>
-                                </visuals:SurfaceControl>
+                                    </visuals:TextureControl.Background>
+                                </visuals:TextureControl>
                             </Border>
                         </Grid>
                     </Border>

+ 3 - 3
src/PixiEditor/Styles/Templates/TimelineGroupHeader.axaml

@@ -23,8 +23,8 @@
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:SurfaceControl
-                                Surface="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
+                            <visuals:TextureControl
+                                Texture="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
                                 Stretch="Uniform" Width="60" Height="60">
                                 <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
@@ -33,7 +33,7 @@
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
                                     </MultiBinding>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                            </visuals:SurfaceControl>
+                            </visuals:TextureControl>
                         </Border>
                         <TextBlock Margin="5 0 0 0" VerticalAlignment="Center" Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
                         <ToggleButton Name="PART_CollapseButton" Margin="0 0 5 0" DockPanel.Dock="Right" Classes="ExpandCollapseToggleStyle" HorizontalAlignment="Right" VerticalAlignment="Center" />

+ 2 - 2
src/PixiEditor/ViewModels/Dock/LayoutManager.cs

@@ -63,7 +63,7 @@ internal class LayoutManager
                         Id = "DocumentArea", FallbackContent = new CreateDocumentFallbackView(),
                         Dockables = [ DockContext.CreateDockable(nodeGraphDockViewModel) ]
                     },
-                    FirstSize = 0.85,
+                    SecondSize = 200,
                     SplitDirection = DockingDirection.Bottom,
                     Second = new DockableArea
                     {
@@ -71,7 +71,7 @@ internal class LayoutManager
                         ActiveDockable = DockContext.CreateDockable(timelineDockViewModel)
                     }
                 },
-                FirstSize = 0.85,
+                SecondSize = 360,
                 SplitDirection = DockingDirection.Right,
                 Second = new DockableTree
                 {

+ 26 - 12
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -168,17 +168,17 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
 
     //TODO: It was DrawingSurface before, check if it's correct
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; } = new()
+    public Dictionary<ChunkResolution, Texture> Surfaces { get; set; } = new()
     {
-        [ChunkResolution.Full] = new Surface(new VecI(64, 64)),
-        [ChunkResolution.Half] = new Surface(new VecI(32, 32)),
-        [ChunkResolution.Quarter] = new Surface(new VecI(16, 16)),
-        [ChunkResolution.Eighth] = new Surface(new VecI(8, 8))
+        [ChunkResolution.Full] = new Texture(new VecI(64, 64)),
+        [ChunkResolution.Half] = new Texture(new VecI(32, 32)),
+        [ChunkResolution.Quarter] = new Texture(new VecI(16, 16)),
+        [ChunkResolution.Eighth] = new Texture(new VecI(8, 8))
     };
 
-    private Surface previewSurface;
+    private Texture previewSurface;
 
-    public Surface PreviewSurface
+    public Texture PreviewSurface
     {
         get => previewSurface;
         set
@@ -237,7 +237,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
 
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
-        PreviewSurface = new Surface(new VecI(previewSize.X, previewSize.Y));
+        PreviewSurface = new Texture(new VecI(previewSize.X, previewSize.Y));
 
         ReferenceLayerViewModel = new(this, Internals);
 
@@ -626,7 +626,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public Color? PickColorFromReferenceLayer(VecD pos)
     {
-        Surface? bitmap = ReferenceLayerViewModel.ReferenceBitmap;
+        Texture? bitmap = ReferenceLayerViewModel.ReferenceBitmap;
         if (bitmap is null)
             return null;
 
@@ -827,18 +827,26 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
     }
 
-    public Image[] RenderFrames(Func<Surface, Surface> processFrameAction = null)
+    public Image[] RenderFrames(Func<Surface, Surface> processFrameAction = null, CancellationToken token = default)
     {
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return [];
+        
+        if(token.IsCancellationRequested)
+            return [];
 
         int firstFrame = AnimationDataViewModel.FirstFrame;
         int framesCount = AnimationDataViewModel.FramesCount;
         int lastFrame = firstFrame + framesCount;
 
         Image[] images = new Image[framesCount];
+        
+        // TODO: Multi-threading
         for (int i = firstFrame; i < lastFrame; i++)
         {
+            if (token.IsCancellationRequested)
+                return [];
+            
             double normalizedTime = (double)(i - firstFrame) / framesCount;
             KeyFrameTime frameTime = new KeyFrameTime(i, normalizedTime);
             var surface = TryRenderWholeImage(frameTime);
@@ -863,7 +871,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     ///     Render frames progressively and disposes the surface after processing.
     /// </summary>
     /// <param name="processFrameAction">Action to perform on rendered frame</param>
-    public void RenderFramesProgressive(Action<Surface, int> processFrameAction)
+    /// <param name="token"></param>
+    public void RenderFramesProgressive(Action<Surface, int> processFrameAction, CancellationToken token)
     {
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return;
@@ -876,7 +885,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         for (int i = firstFrame; i < lastFrame; i++)
         {
-            var surface = TryRenderWholeImage(i);
+            if (token.IsCancellationRequested)
+                return;
+            
+            KeyFrameTime frameTime = new KeyFrameTime(i, (double)(i - firstFrame) / framesCount);
+            
+            var surface = TryRenderWholeImage(frameTime);
             if (surface.IsT0)
             {
                 continue;

+ 2 - 2
src/PixiEditor/ViewModels/Document/KeyFrameViewModel.cs

@@ -9,7 +9,7 @@ namespace PixiEditor.ViewModels.Document;
 
 internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
 {
-    private Surface? previewSurface;
+    private Texture? previewSurface;
     private int startFrameBindable;
     private int durationBindable;
     private bool isVisibleBindable = true;
@@ -27,7 +27,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
 
     IDocument IKeyFrameHandler.Document => Document;
 
-    public Surface? PreviewSurface
+    public Texture? PreviewSurface
     {
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);

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

@@ -26,7 +26,7 @@ internal class ReferenceLayerViewModel : ObservableObject, IReferenceLayerHandle
 
     public const double TopMostOpacity = 0.6;
     
-    public Surface? ReferenceBitmap { get; private set; }
+    public Texture? ReferenceBitmap { get; private set; }
 
     private ShapeCorners referenceShape;
     public ShapeCorners ReferenceShapeBindable 
@@ -114,7 +114,7 @@ internal class ReferenceLayerViewModel : ObservableObject, IReferenceLayerHandle
     
     public void SetReferenceLayer(ImmutableArray<byte> imageBgra8888Bytes, VecI imageSize, ShapeCorners shape)
     {
-        ReferenceBitmap = Surface.Load(imageBgra8888Bytes.ToArray(), ColorType.Bgra8888, imageSize); //TODO: Was WriteableBitmapUtility.FromBgra8888Array(imageBgra8888Bytes.ToArray(), imageSize);
+        ReferenceBitmap = Texture.Load(imageBgra8888Bytes.ToArray(), ColorType.Bgra8888, imageSize); 
         referenceShape = shape;
         isVisible = true;
         isTransforming = false;

+ 5 - 4
src/PixiEditor/ViewModels/Document/StructureMemberViewModel.cs

@@ -6,6 +6,7 @@ using PixiEditor.Views.Nodes;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
@@ -143,16 +144,16 @@ internal abstract class StructureMemberViewModel : NodeViewModel, IStructureMemb
         set => SetProperty(ref selection, value);
     }
 
-    private Surface? previewSurface;
-    private Surface? maskPreviewSurface;
+    private Texture? previewSurface;
+    private Texture? maskPreviewSurface;
 
-    public Surface? PreviewSurface
+    public Texture? PreviewSurface
     {
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);
     }
 
-    public Surface? MaskPreviewSurface
+    public Texture? MaskPreviewSurface
     {
         get => maskPreviewSurface;
         set => SetProperty(ref maskPreviewSurface, value);

+ 2 - 2
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -19,7 +19,7 @@ internal class NodeViewModel : ObservableObject, INodeHandler
     private VecD position;
     private ObservableRangeCollection<INodePropertyHandler> inputs = new();
     private ObservableRangeCollection<INodePropertyHandler> outputs = new();
-    private Surface resultPreview;
+    private Texture resultPreview;
     private bool isSelected;
 
     protected Guid id;
@@ -71,7 +71,7 @@ internal class NodeViewModel : ObservableObject, INodeHandler
         set => SetProperty(ref outputs, value);
     }
 
-    public Surface ResultPreview
+    public Texture ResultPreview
     {
         get => resultPreview;
         set => SetProperty(ref resultPreview, value);

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -7,6 +7,7 @@ using Avalonia.Input;
 using Avalonia.Media;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Clipboard;
 using PixiEditor.Models.Commands.Attributes.Commands;
@@ -82,7 +83,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
         // TODO: Exception handling would probably be good
-        var bitmap = Importer.GetPreviewBitmap(path);
+        var bitmap = Importer.GetPreviewSurface(path);
         byte[] pixels = bitmap.ToWriteableBitmap().ExtractPixels();
 
         doc.Operations.ImportReferenceLayer(

+ 21 - 8
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -8,6 +9,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Platform.Storage;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using Newtonsoft.Json.Linq;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
@@ -362,7 +364,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         string finalPath = null;
         if (asNew || string.IsNullOrEmpty(document.FullFilePath))
         {
-            var result = await Exporter.TrySaveWithDialog(document, ExportConfig.Empty);
+            var result = await Exporter.TrySaveWithDialog(document, ExportConfig.Empty, null);
             if (result.Result == DialogSaveResult.Cancelled)
                 return false;
             if (result.Result != DialogSaveResult.Success)
@@ -376,7 +378,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         else
         {
-            var result = await Exporter.TrySaveAsync(document, document.FullFilePath, ExportConfig.Empty);
+            var result = await Exporter.TrySaveAsync(document, document.FullFilePath, ExportConfig.Empty, null);
             if (result != SaveResult.Success)
             {
                 ShowSaveError((DialogSaveResult)result);
@@ -412,12 +414,23 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             };
             if (await info.ShowDialog())
             {
-                var result =
-                    await Exporter.TrySaveUsingDataFromDialog(doc, info.FilePath, info.ChosenFormat, info.ExportConfig);
-                if (result.result == SaveResult.Success)
-                    IOperatingSystem.Current.OpenFolder(result.finalPath);
-                else
-                    ShowSaveError((DialogSaveResult)result.result);
+                ExportJob job = new ExportJob();
+                ProgressDialog dialog = new ProgressDialog(job, MainWindow.Current);
+
+                Task.Run(async () =>
+                {
+                    var result =
+                        await Exporter.TrySaveUsingDataFromDialog(doc, info.FilePath, info.ChosenFormat,
+                            info.ExportConfig,
+                            job);
+                    
+                    if (result.result == SaveResult.Success)
+                        IOperatingSystem.Current.OpenFolder(result.finalPath);
+                    else
+                        ShowSaveError((DialogSaveResult)result.result);
+                });
+                
+                await dialog.ShowDialog();
             }
         }
         catch (RecoverableException e)

+ 3 - 3
src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -80,7 +80,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         Document = document;
         Document.SizeChanged += DocumentOnSizeChanged;
         Document.PropertyChanged += DocumentOnPropertyChanged;
-        TabCustomizationSettings.Icon = new SurfaceImage(Document.PreviewSurface);
+        TabCustomizationSettings.Icon = new TextureImage(Document.PreviewSurface);
     }
 
     private void DocumentOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -91,7 +91,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         }
         else if (e.PropertyName == nameof(DocumentViewModel.PreviewSurface))
         {
-            TabCustomizationSettings.Icon = new SurfaceImage(Document.PreviewSurface);
+            TabCustomizationSettings.Icon = new TextureImage(Document.PreviewSurface);
         }
         else if (e.PropertyName == nameof(DocumentViewModel.AllChangesSaved))
         {
@@ -107,7 +107,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
 
     private void DocumentOnSizeChanged(object? sender, DocumentSizeChangedEventArgs e)
     {
-        TabCustomizationSettings.Icon = new SurfaceImage(Document.PreviewSurface);
+        TabCustomizationSettings.Icon = new TextureImage(Document.PreviewSurface);
         OnPropertyChanged(nameof(TabCustomizationSettings));
     }
 

+ 62 - 24
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -192,7 +192,7 @@ internal partial class ExportFilePopup : PixiEditorPopup
         videoPreviewTimer.Stop();
         videoPreviewTimer.Tick -= OnVideoPreviewTimerOnTick;
         videoPreviewTimer = null;
-        cancellationTokenSource.Dispose();
+        cancellationTokenSource.Cancel();
 
         ExportPreview?.Dispose();
 
@@ -210,6 +210,11 @@ internal partial class ExportFilePopup : PixiEditorPopup
         if (videoPreviewFrames.Length > 0)
         {
             ExportPreview.DrawingSurface.Canvas.Clear();
+            if (videoPreviewFrames[activeFrame] == null)
+            {
+                return;
+            }
+            
             ExportPreview.DrawingSurface.Canvas.DrawImage(videoPreviewFrames[activeFrame], 0, 0);
             activeFrame = (activeFrame + 1) % videoPreviewFrames.Length;
         }
@@ -232,7 +237,8 @@ internal partial class ExportFilePopup : PixiEditorPopup
         if (IsVideoExport)
         {
             StartRenderAnimationJob();
-            videoPreviewTimer.Interval = TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRateBindable);
+            videoPreviewTimer.Interval =
+                TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRateBindable);
         }
         else
         {
@@ -242,43 +248,72 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
     private void RenderImagePreview()
     {
-        if (IsSpriteSheetExport)
-        {
-            GenerateSpriteSheetPreview();
-        }
-        else
+        try
         {
-            var rendered = document.TryRenderWholeImage(0);
-            if (rendered.IsT1)
+            if (IsSpriteSheetExport)
             {
-                VecI previewSize = CalculatePreviewSize(rendered.AsT1.Size);
-                ExportPreview = rendered.AsT1.ResizeNearestNeighbor(previewSize);
-                rendered.AsT1.Dispose();
+                GenerateSpriteSheetPreview();
+            }
+            else
+            {
+                Task.Run(() =>
+                {
+                    var rendered = document.TryRenderWholeImage(0);
+                    if (rendered.IsT1)
+                    {
+                        VecI previewSize = CalculatePreviewSize(rendered.AsT1.Size);
+                        Dispatcher.UIThread.Post(() =>
+                        {
+                            ExportPreview = rendered.AsT1.ResizeNearestNeighbor(previewSize);
+                            rendered.AsT1.Dispose();
+                        });
+                    }
+                });
             }
         }
-
-        IsGeneratingPreview = false;
+        finally
+        {
+            IsGeneratingPreview = false;
+        }
     }
 
     private void GenerateSpriteSheetPreview()
     {
         int clampedColumns = Math.Max(SpriteSheetColumns, 1);
         int clampedRows = Math.Max(SpriteSheetRows, 1);
-        
+
         VecI previewSize = CalculatePreviewSize(new VecI(SaveWidth * clampedColumns, SaveHeight * clampedRows));
-        VecI singleFrameSize = new VecI(previewSize.X / Math.Max(clampedColumns, 1), previewSize.Y / Math.Max(clampedRows, 1));
+        VecI singleFrameSize = new VecI(previewSize.X / Math.Max(clampedColumns, 1),
+            previewSize.Y / Math.Max(clampedRows, 1));
         if (previewSize != ExportPreview.Size)
         {
             ExportPreview?.Dispose();
             ExportPreview = new Surface(previewSize);
 
-            document.RenderFramesProgressive((frame, index) =>
+            Task.Run(() =>
             {
-                int x = index % clampedColumns;
-                int y = index / clampedColumns;
-                var resized = frame.ResizeNearestNeighbor(new VecI(singleFrameSize.X, singleFrameSize.Y));
-                ExportPreview!.DrawingSurface.Canvas.DrawSurface(resized.DrawingSurface, x * singleFrameSize.X, y * singleFrameSize.Y);
-                resized.Dispose();
+                try
+                {
+                    document.RenderFramesProgressive(
+                        (frame, index) =>
+                        {
+                            int x = index % clampedColumns;
+                            int y = index / clampedColumns;
+                            var resized = frame.ResizeNearestNeighbor(new VecI(singleFrameSize.X, singleFrameSize.Y));
+                            Dispatcher.UIThread.Post(() =>
+                            {
+                                if (ExportPreview.IsDisposed) return;
+                                ExportPreview!.DrawingSurface.Canvas.DrawSurface(resized.DrawingSurface,
+                                    x * singleFrameSize.X,
+                                    y * singleFrameSize.Y);
+                                resized.Dispose();
+                            });
+                        }, cancellationTokenSource.Token);
+                }
+                catch 
+                {
+                    // Ignore
+                }
             });
         }
     }
@@ -295,7 +330,7 @@ internal partial class ExportFilePopup : PixiEditorPopup
         Task.Run(
             () =>
             {
-                videoPreviewFrames = document.RenderFrames(ProcessFrame);
+                videoPreviewFrames = document.RenderFrames(ProcessFrame, cancellationTokenSource.Token);
             }, cancellationTokenSource.Token).ContinueWith(_ =>
         {
             Dispatcher.UIThread.Invoke(() =>
@@ -429,7 +464,10 @@ internal partial class ExportFilePopup : PixiEditorPopup
     {
         if (e.Sender is ExportFilePopup popup)
         {
-            popup.RenderPreview();
+            if (popup.videoPreviewTimer != null)
+            {
+                popup.RenderPreview();
+            }
         }
     }
 }

+ 48 - 0
src/PixiEditor/Views/Dialogs/ProgressDialog.cs

@@ -0,0 +1,48 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+
+namespace PixiEditor.Views.Dialogs;
+
+internal class ProgressDialog : CustomDialog
+{
+    public ExportJob Job { get; }
+    
+    public ProgressDialog(ExportJob job, Window ownerWindow) : base(ownerWindow)
+    {
+        Job = job;
+    }
+
+    public override async Task<bool> ShowDialog()
+    {
+        ProgressPopup popup = new ProgressPopup();
+        popup.CancellationToken = Job.CancellationTokenSource;
+        Job.ProgressChanged += (progress, status) =>
+        {
+            Dispatcher.UIThread.Post(() =>
+            {
+                popup.Progress = progress;
+                popup.Status = status;
+            });
+        };
+        
+        Job.Finished += () =>
+        {
+            Dispatcher.UIThread.Post(() =>
+            {
+                popup.Close();
+            });
+        };
+        
+        Job.Cancelled += () =>
+        {
+            Dispatcher.UIThread.Post(() =>
+            {
+                popup.Close();
+            });
+        };
+        
+        return await popup.ShowDialog<bool>(OwnerWindow);
+    }
+}

+ 17 - 0
src/PixiEditor/Views/Dialogs/ProgressPopup.axaml

@@ -0,0 +1,17 @@
+<dialogs:PixiEditorPopup xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+        x:Class="PixiEditor.Views.Dialogs.ProgressPopup"
+        CanMinimize="False"
+        CanResize="False"
+        ui:Translator.Key="PROGRESS_POPUP_TITLE"
+        Width="400" Height="150">
+    <StackPanel DataContext="{Binding RelativeSource={RelativeSource AncestorType=dialogs:ProgressPopup, Mode=FindAncestor}}">
+        <TextBlock ui:Translator.Key="{Binding Status}" Margin="10" Classes="h3"/>
+        <ProgressBar VerticalAlignment="Center" ShowProgressText="True" Value="{Binding Progress}" Maximum="100" Margin="10"/>
+    </StackPanel>
+</dialogs:PixiEditorPopup>

+ 51 - 0
src/PixiEditor/Views/Dialogs/ProgressPopup.axaml.cs

@@ -0,0 +1,51 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.Views.Dialogs;
+
+public partial class ProgressPopup : PixiEditorPopup
+{
+    public static readonly StyledProperty<double> ProgressProperty = AvaloniaProperty.Register<ProgressPopup, double>(
+        nameof(Progress));
+
+    public static readonly StyledProperty<string> StatusProperty = AvaloniaProperty.Register<ProgressPopup, string>(
+        nameof(Status));
+
+    public static readonly StyledProperty<CancellationTokenSource> CancellationTokenProperty = AvaloniaProperty.Register<ProgressPopup, CancellationTokenSource>(
+        nameof(CancellationToken));
+
+    public CancellationTokenSource CancellationToken
+    {
+        get => GetValue(CancellationTokenProperty);
+        set => SetValue(CancellationTokenProperty, value);
+    }
+
+    public string Status
+    {
+        get => GetValue(StatusProperty);
+        set => SetValue(StatusProperty, value);
+    }
+
+    public double Progress
+    {
+        get => GetValue(ProgressProperty);
+        set => SetValue(ProgressProperty, value);
+    }
+
+    protected override void OnClosing(WindowClosingEventArgs e)
+    {
+        base.OnClosing(e);
+        CancellationToken.Cancel();
+        if(!e.IsProgrammatic)
+        {
+            e.Cancel = true;
+        }
+    }
+
+    public ProgressPopup()
+    {
+        InitializeComponent();
+    }
+}
+

+ 4 - 3
src/PixiEditor/Views/Layers/FolderControl.axaml

@@ -65,14 +65,15 @@
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:SurfaceControl Surface="{Binding Folder.PreviewSurface, ElementName=folderControl}" Stretch="Uniform" Width="30" Height="30">
+                            <visuals:TextureControl Texture="{Binding Folder.PreviewSurface, ElementName=folderControl}" 
+                                                    Stretch="Uniform" Width="30" Height="30">
                                 <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
                                         <Binding Path="Folder.PreviewSurface.Size.X" ElementName="folderControl"/>
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width"/>
                                     </MultiBinding>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                            </visuals:SurfaceControl>
+                            </visuals:TextureControl>
                         </Border>
                         <Border 
                             Width="32" Height="32" 
@@ -89,7 +90,7 @@
                                 </ImageBrush>
                             </Border.Background>
                             <Grid IsHitTestVisible="False">
-                                <visuals:SurfaceControl Surface="{Binding Folder.MaskPreviewSurface, ElementName=folderControl}" Stretch="Uniform" Width="30" Height="30"
+                                <visuals:TextureControl Texture="{Binding Folder.MaskPreviewSurface, ElementName=folderControl}" Stretch="Uniform" Width="30" Height="30"
                                     RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                                 <Path 
                                 Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z" 

+ 2 - 2
src/PixiEditor/Views/Layers/LayerControl.axaml

@@ -75,7 +75,7 @@
                                 <Binding ElementName="uc" Path="Layer.HasMaskBindable"/>
                             </MultiBinding>
                         </Border.BorderBrush>
-                        <visuals:SurfaceControl Surface="{Binding Layer.PreviewSurface, ElementName=uc}"
+                        <visuals:TextureControl Texture="{Binding Layer.PreviewSurface, ElementName=uc}"
                                                 Stretch="Uniform" Width="30"
                                                 Height="30"
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
@@ -100,7 +100,7 @@
                             </MultiBinding>
                         </Border.BorderBrush>
                         <Grid IsHitTestVisible="False">
-                            <visuals:SurfaceControl Surface="{Binding Layer.MaskPreviewSurface,ElementName=uc}" Stretch="Uniform" Width="30" Height="30"
+                            <visuals:TextureControl Texture="{Binding Layer.MaskPreviewSurface,ElementName=uc}" Stretch="Uniform" Width="30" Height="30"
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                             <Path
                                 Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z"

+ 1 - 1
src/PixiEditor/Views/Layers/ReferenceLayer.axaml

@@ -80,7 +80,7 @@
                             BorderBrush="Black"
                             Background="{DynamicResource ThemeBackgroundBrush}"
                             Margin="5, 0, 10, 0">
-                            <visuals:SurfaceControl Surface="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap,ElementName=uc}"
+                            <visuals:TextureControl Texture="{Binding Document.ReferenceLayerViewModel.ReferenceBitmap,ElementName=uc}"
                                    Stretch="Uniform" Width="26" Height="26"
                                    RenderOptions.BitmapInterpolationMode="HighQuality" IsHitTestVisible="False" />
                         </Border>

+ 3 - 1
src/PixiEditor/Views/Main/Tools/ToolPickerButton.axaml

@@ -7,6 +7,7 @@
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              xmlns:tools="clr-namespace:PixiEditor.ViewModels.Tools"
              xmlns:markupExtensions="clr-namespace:PixiEditor.Helpers.MarkupExtensions"
+             xmlns:ui1="clr-namespace:PixiEditor.Helpers.UI"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              d:DataContext="{tools:ToolViewModel}"
              x:Class="PixiEditor.Views.Main.Tools.ToolPickerButton">
@@ -20,7 +21,8 @@
             Background="{DynamicResource ThemeBackgroundBrush1}">
         <Button.Template>
             <ControlTemplate>
-                <Border Height="40" Width="40" Background="{DynamicResource ThemeBackgroundBrush1}">
+                <Border Height="40" Width="40" 
+                        Background="{DynamicResource ThemeBackgroundBrush1}">
                     <ContentPresenter Content="{TemplateBinding Content}"/>
                 </Border>
             </ControlTemplate>

+ 3 - 3
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml

@@ -14,10 +14,10 @@
              VerticalAlignment="Center"
              d:DesignHeight="450" d:DesignWidth="800">
 
-    <visuals:SurfaceControl
+    <visuals:TextureControl
         x:Name="mainImage"
         Focusable="True"
-        Surface="{Binding TargetBitmap, ElementName=uc}"
+        Texture="{Binding TargetBitmap, ElementName=uc}"
         Stretch="Uniform"
         SizeChanged="OnImageSizeChanged">
         <ui1:RenderOptionsBindable.BitmapInterpolationMode>
@@ -26,5 +26,5 @@
                 <Binding ElementName="mainImage" Path="Bounds.Width"/>
             </MultiBinding>
         </ui1:RenderOptionsBindable.BitmapInterpolationMode>
-    </visuals:SurfaceControl>
+    </visuals:TextureControl>
 </UserControl>

+ 9 - 11
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs

@@ -1,14 +1,12 @@
-using System.Collections.Generic;
-using System.ComponentModel;
+using System.ComponentModel;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Interactivity;
-using Avalonia.Media.Imaging;
-using ChunkyImageLib;
+using Avalonia.Media;
+using Avalonia.Threading;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Position;
 using PixiEditor.Numerics;
@@ -21,8 +19,8 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
         AvaloniaProperty.Register<FixedViewport, DocumentViewModel>(nameof(Document), null);
 
-    private static readonly StyledProperty<Dictionary<ChunkResolution, WriteableBitmap>> BitmapsProperty =
-        AvaloniaProperty.Register<FixedViewport, Dictionary<ChunkResolution, WriteableBitmap>>(nameof(Bitmaps), null);
+    private static readonly StyledProperty<Dictionary<ChunkResolution, Texture>> BitmapsProperty =
+        AvaloniaProperty.Register<FixedViewport, Dictionary<ChunkResolution, Texture>>(nameof(Bitmaps), null);
 
     public static readonly StyledProperty<bool> DelayedProperty =
         AvaloniaProperty.Register<FixedViewport, bool>(nameof(Delayed), false);
@@ -35,7 +33,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DelayedProperty, value);
     }
 
-    public Dictionary<ChunkResolution, WriteableBitmap>? Bitmaps
+    public Dictionary<ChunkResolution, Texture>? Bitmaps
     {
         get => GetValue(BitmapsProperty);
         set => SetValue(BitmapsProperty, value);
@@ -47,11 +45,11 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DocumentProperty, value);
     }
 
-    public Surface? TargetBitmap
+    public Texture? TargetBitmap
     {
         get
         {
-            if (Document?.Surfaces.TryGetValue(CalculateResolution(), out Surface? value) == true)
+            if (Document?.Surfaces.TryGetValue(CalculateResolution(), out Texture? value) == true)
                 return value;
             return null;
         }
@@ -146,7 +144,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         Document?.Operations.AddOrUpdateViewport(GetLocation());
     }
 
-    private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, WriteableBitmap>> args)
+    private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, Texture>> args)
     {
         FixedViewport? viewport = (FixedViewport)args.Sender;
         viewport.PropertyChanged?.Invoke(viewport, new(nameof(TargetBitmap)));

+ 2 - 2
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -267,11 +267,11 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         }
     }
 
-    public Surface? TargetBitmap
+    public Texture? TargetBitmap
     {
         get
         {
-            return Document?.Surfaces.TryGetValue(CalculateResolution(), out Surface? value) == true ? value : null;
+            return Document?.Surfaces.TryGetValue(CalculateResolution(), out Texture? value) == true ? value : null;
         }
     }
 

+ 40 - 4
src/PixiEditor/Views/MainWindow.axaml.cs

@@ -4,11 +4,16 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
+using Avalonia.OpenGL;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
+using Avalonia.Vulkan;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.IO;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.DrawingApi.Skia.Implementations;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Helpers;
@@ -31,10 +36,15 @@ internal partial class MainWindow : Window
 
     private StartupPerformance _startupPerformance = new();
     
-    public new ViewModels_ViewModelMain DataContext { get => (ViewModels_ViewModelMain)base.DataContext; set => base.DataContext = value; }
-    
-    public static MainWindow? Current {
-        get 
+    public new ViewModels_ViewModelMain DataContext
+    {
+        get => (ViewModels_ViewModelMain)base.DataContext;
+        set => base.DataContext = value;
+    }
+
+    public static MainWindow? Current
+    {
+        get
         {
             if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
                 return desktop.MainWindow as MainWindow;
@@ -60,6 +70,7 @@ internal partial class MainWindow : Window
         AsyncImageLoader.ImageLoader.AsyncImageLoader = new DiskCachedWebImageLoader();
 
         SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
+        skiaDrawingBackend.GraphicsContext = GetOpenGlGrContext();
         DrawingBackendApi.SetupBackend(skiaDrawingBackend);
 
         preferences = services.GetRequiredService<IPreferences>();
@@ -74,6 +85,31 @@ internal partial class MainWindow : Window
         InitializeComponent();
     }
 
+
+    public static GRContext GetOpenGlGrContext()
+    {
+        Compositor compositor = Compositor.TryGetDefaultCompositor();
+        var interop = compositor.TryGetCompositionGpuInterop();
+        var contextSharingFeature =
+            compositor.TryGetRenderInterfaceFeature(typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)).Result
+                as IOpenGlTextureSharingRenderInterfaceContextFeature;
+
+        if (contextSharingFeature.CanCreateSharedContext)
+        {
+            IGlContext? glContext = contextSharingFeature.CreateSharedContext();
+            glContext.MakeCurrent();
+            return GRContext.CreateGl(GRGlInterface.Create(glContext.GlInterface.GetProcAddress));
+        }
+
+        return null;
+        /*var contextFactory = AvaloniaLocator.Current.GetRequiredService<IPlatformGraphicsOpenGlContextFactory>();
+        var ctx = contextFactory.CreateContext(null);
+        ctx.MakeCurrent();
+        var ctxInterface = GRGlInterface.Create(ctx.GlInterface.GetProcAddress);
+        var grContext = GRContext.CreateGl(ctxInterface);
+        return grContext;*/
+    }
+
     public static MainWindow CreateWithRecoveredDocuments(CrashReport report, out bool showMissingFilesDialog)
     {
         var window = GetMainWindow();

+ 2 - 2
src/PixiEditor/Views/Nodes/NodeView.cs

@@ -34,7 +34,7 @@ public class NodeView : TemplatedControl
         AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
             nameof(Outputs));
 
-    public static readonly StyledProperty<Surface> ResultPreviewProperty = AvaloniaProperty.Register<NodeView, Surface>(
+    public static readonly StyledProperty<Texture> ResultPreviewProperty = AvaloniaProperty.Register<NodeView, Texture>(
         nameof(ResultPreview));
 
     public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<NodeView, bool>(
@@ -64,7 +64,7 @@ public class NodeView : TemplatedControl
         set => SetValue(IsSelectedProperty, value);
     }
 
-    public Surface ResultPreview
+    public Texture ResultPreview
     {
         get => GetValue(ResultPreviewProperty);
         set => SetValue(ResultPreviewProperty, value);

+ 21 - 2
src/PixiEditor/Views/Overlays/ReferenceLayerOverlay.cs

@@ -9,17 +9,22 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Media;
 using Avalonia.Styling;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Helpers.Converters;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.Views.Visuals;
+using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 
 namespace PixiEditor.Views.Overlays;
 
 internal class ReferenceLayerOverlay : Overlay
 {
     private const float OpacityTransitionDuration = 0.1f;
+
     public static readonly StyledProperty<ReferenceLayerViewModel> ReferenceLayerProperty =
         AvaloniaProperty.Register<ReferenceLayerOverlay, ReferenceLayerViewModel>(
             nameof(ReferenceLayerViewModel));
@@ -59,6 +64,12 @@ internal class ReferenceLayerOverlay : Overlay
         : OverlayRenderSorting.Background;
 
     private Pen borderBen = new Pen(Brushes.Black, 2);
+    
+    private Paint overlayPaint = new Paint
+    {
+        Color = new Color(255, 255, 255, 255),
+        BlendMode = BlendMode.SrcOver
+    };
 
     static ReferenceLayerOverlay()
     {
@@ -79,8 +90,16 @@ internal class ReferenceLayerOverlay : Overlay
 
             RectD dirty = new RectD(0, 0, ReferenceLayer.ReferenceBitmap.Size.X, ReferenceLayer.ReferenceBitmap.Size.Y);
             Rect dirtyRect = new Rect(dirty.X, dirty.Y, dirty.Width, dirty.Height);
-            DrawSurfaceOperation drawOperation =
-                new DrawSurfaceOperation(dirtyRect, ReferenceLayer.ReferenceBitmap, Stretch.None, Opacity);
+
+            double opacity = Opacity;
+            var referenceBitmap = ReferenceLayer.ReferenceBitmap;
+
+            referenceBitmap.Surface.Flush();
+            overlayPaint.Color = new Color(255, 255, 255, (byte)(opacity * 255)); 
+            
+            DrawTextureOperation drawOperation =
+                new DrawTextureOperation(dirtyRect, Stretch.None, referenceBitmap, overlayPaint);
+
             context.Custom(drawOperation);
 
             matrix.Dispose();

+ 11 - 3
src/PixiEditor/Views/Palettes/PaletteColorControl.axaml.cs

@@ -56,9 +56,17 @@ internal partial class PaletteColorControl : UserControl
             float length = (float)Math.Sqrt(movedDistance.X * movedDistance.X + movedDistance.Y * movedDistance.Y);
             if (length > 10)
             {
-                DataObject data = new DataObject();
-                data.Set(PaletteColorDaoFormat, colorControl.Color.ToString());
-                DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
+                try
+                {
+                    DataObject data = new DataObject();
+                    data.Set(PaletteColorDaoFormat, colorControl.Color.ToString());
+                    DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
+                }
+                catch
+                {
+                    // ignored
+                }
+
                 e.Handled = true;
             }
         }

+ 1 - 0
src/PixiEditor/Views/Palettes/PaletteViewer.axaml.cs

@@ -286,6 +286,7 @@ internal partial class PaletteViewer : UserControl
                     Colors.RemoveAt(currIndex);
                     Colors.Insert(newIndex, paletteColor);
                     int indexOfSource = Colors.IndexOf(paletteColorControl.Color);
+                    if(indexOfSource < 0) return;
                     Colors.Move(indexOfSource, currIndex);
                 }
             }

+ 27 - 10
src/PixiEditor/Views/Rendering/Scene.cs

@@ -13,8 +13,10 @@ using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.DrawingApi.Skia.Extensions;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Converters;
@@ -32,7 +34,7 @@ namespace PixiEditor.Views.Rendering;
 
 internal class Scene : Zoombox.Zoombox, ICustomHitTest
 {
-    public static readonly StyledProperty<Surface> SurfaceProperty = AvaloniaProperty.Register<SurfaceControl, Surface>(
+    public static readonly StyledProperty<Texture> SurfaceProperty = AvaloniaProperty.Register<SurfaceControl, Texture>(
         nameof(Surface));
 
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
@@ -86,7 +88,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(DocumentProperty, value);
     }
 
-    public Surface Surface
+    public Texture Surface
     {
         get => GetValue(SurfaceProperty);
         set => SetValue(SurfaceProperty, value);
@@ -145,6 +147,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         RectD dirtyBounds = new RectD(0, 0, Document.Width / resolutionScale, Document.Height / resolutionScale);
         Rect dirtyRect = new Rect(0, 0, Document.Width / resolutionScale, Document.Height / resolutionScale);
 
+        Surface.Surface.Flush();
         using var operation = new DrawSceneOperation(Surface, Document, CanvasPos, Scale * resolutionScale, angle,
             FlipX, FlipY,
             dirtyRect,
@@ -185,9 +188,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                 }
 
                 overlay.ZoomScale = Scale;
-                
-                if(!overlay.CanRender()) continue;
-                
+
+                if (!overlay.CanRender()) continue;
+
                 overlay.RenderOverlay(context, dirtyBounds);
                 Cursor = overlay.Cursor ?? DefaultCursor;
             }
@@ -453,7 +456,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     private static void SurfaceChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
     {
-        if (e.NewValue is Surface surface)
+        if (e.NewValue is Texture surface)
         {
             scene.ContentDimensions = surface.Size;
         }
@@ -475,7 +478,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
 internal class DrawSceneOperation : SkiaDrawOperation
 {
-    public Surface Surface { get; set; }
+    public Texture Surface { get; set; }
     public DocumentViewModel Document { get; set; }
     public VecD ContentPosition { get; set; }
     public double Scale { get; set; }
@@ -489,7 +492,9 @@ internal class DrawSceneOperation : SkiaDrawOperation
 
     private SKPaint _paint = new SKPaint();
 
-    public DrawSceneOperation(Surface surface, DocumentViewModel document, VecD contentPosition, double scale,
+    private bool hardwareAccelerationAvailable = DrawingBackendApi.Current.IsHardwareAccelerated;
+
+    public DrawSceneOperation(Texture surface, DocumentViewModel document, VecD contentPosition, double scale,
         double angle, bool flipX, bool flipY, Rect dirtyBounds, Rect viewportBounds, double opacity,
         ColorMatrix colorMatrix) : base(dirtyBounds)
     {
@@ -520,13 +525,25 @@ internal class DrawSceneOperation : SkiaDrawOperation
             return;
         }
 
-        using Image snapshot = Surface.DrawingSurface.Snapshot(SurfaceRectToRender);
+        using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);
+
 
         var matrixValues = new float[ColorMatrix.Width * ColorMatrix.Height];
         ColorMatrix.TryGetMembers(matrixValues);
 
         _paint.ColorFilter = SKColorFilter.CreateColorMatrix(matrixValues);
-        canvas.DrawImage((SKImage)snapshot.Native, SurfaceRectToRender.X, SurfaceRectToRender.Y, _paint);
+
+        if (!hardwareAccelerationAvailable)
+        {
+            // snapshotting wanted region on CPU is faster than rendering whole surface on CPU,
+            // but slower than rendering whole surface on GPU
+            using Image snapshot = Surface.Surface.Snapshot(SurfaceRectToRender);
+            canvas.DrawImage((SKImage)snapshot.Native, SurfaceRectToRender.X, SurfaceRectToRender.Y, _paint);
+        }
+        else
+        {
+            canvas.DrawSurface(Surface.Surface.Native as SKSurface, 0, 0, _paint);
+        }
 
         canvas.Restore();
     }

+ 206 - 0
src/PixiEditor/Views/Visuals/TextureControl.cs

@@ -0,0 +1,206 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using Avalonia.Threading;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Bridge;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.DrawingApi.Skia.Extensions;
+using PixiEditor.DrawingApi.Skia.Implementations;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Views.Visuals;
+
+public class TextureControl : Control
+{
+    public static readonly StyledProperty<Texture> TextureProperty = AvaloniaProperty.Register<TextureControl, Texture>(
+        nameof(Texture));
+
+    public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<TextureControl, Stretch>(
+        nameof(Stretch), Stretch.Uniform);
+
+    public static readonly StyledProperty<IBrush> BackgroundProperty = AvaloniaProperty.Register<TextureControl, IBrush>
+    (nameof(Background));
+
+    public Stretch Stretch
+    {
+        get => GetValue(StretchProperty);
+        set => SetValue(StretchProperty, value);
+    }
+
+    public Texture Texture
+    {
+        get => GetValue(TextureProperty);
+        set => SetValue(TextureProperty, value);
+    }
+
+    public IBrush Background
+    {
+        get { return (IBrush)GetValue(BackgroundProperty); }
+        set { SetValue(BackgroundProperty, value); }
+    }
+
+    static TextureControl()
+    {
+        AffectsRender<TextureControl>(TextureProperty, StretchProperty);
+    }
+
+    public TextureControl()
+    {
+        ClipToBounds = true;
+        TextureProperty.Changed.Subscribe(OnTextureChanged);
+    }
+
+    /// <summary>
+    /// Measures the control.
+    /// </summary>
+    /// <param name="availableSize">The available size.</param>
+    /// <returns>The desired size of the control.</returns>
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        var source = Texture;
+        var result = new Size();
+
+        if (source != null)
+        {
+            result = Stretch.CalculateSize(availableSize, new Size(source.Size.X, source.Size.Y));
+        }
+        else if (Width > 0 && Height > 0)
+        {
+            result = Stretch.CalculateSize(availableSize, new Size(Width, Height));
+        }
+
+        return result;
+    }
+
+    /// <inheritdoc/>
+    protected override Size ArrangeOverride(Size finalSize)
+    {
+        var source = Texture;
+
+        if (source != null)
+        {
+            var sourceSize = source.Size;
+            var result = Stretch.CalculateSize(finalSize, new Size(sourceSize.X, sourceSize.Y));
+            return result;
+        }
+        else
+        {
+            return Stretch.CalculateSize(finalSize, new Size(Width, Height));
+        }
+
+        return new Size();
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        if (Background != null)
+        {
+            context.FillRectangle(Background, new Rect(Bounds.Size));
+        }
+        
+        if (Texture == null || Texture.IsDisposed)
+        {
+            return;
+        }
+        
+        Texture texture = Texture;
+        texture.Surface.Flush();
+        ICustomDrawOperation drawOperation = new DrawTextureOperation(
+            new Rect(0, 0, Bounds.Width, Bounds.Height),
+            Stretch,
+            texture);
+
+        context.Custom(drawOperation);
+    }
+    
+    private void OnTextureChanged(AvaloniaPropertyChangedEventArgs<Texture> args)
+    {
+        if (args.OldValue.Value != null)
+        {
+            args.OldValue.Value.Changed -= TextureOnChanged;
+        }
+        
+        if (args.NewValue.Value != null)
+        {
+            args.NewValue.Value.Changed += TextureOnChanged;
+        }
+    }
+    
+    private void TextureOnChanged(RectD? changedRect)
+    {
+        Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
+    }
+}
+
+internal class DrawTextureOperation : SkiaDrawOperation
+{
+    public Stretch Stretch { get; }
+    public VecD TargetSize { get; }
+    public Texture? Texture { get; }
+    public Paint? Paint { get; }
+
+    public DrawTextureOperation(Rect dirtyBounds, Stretch stretch, Texture texture, Paint paint = null) :
+        base(dirtyBounds)
+    {
+        Stretch = stretch;
+        Texture = texture;
+        TargetSize = new VecD(texture.Size.X, texture.Size.Y);
+        Paint = paint;
+    }
+
+    public override void Render(ISkiaSharpApiLease lease)
+    {
+        if (Texture == null || Texture.IsDisposed)
+        {
+            return;
+        }
+        
+        SKCanvas canvas = lease.SkCanvas;
+
+        using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);
+
+        canvas.Save();
+        ScaleCanvas(canvas);
+        canvas.DrawSurface(Texture.Surface.Native as SKSurface, 0, 0, Paint?.Native as SKPaint ?? null);
+        canvas.Restore();
+    }
+
+    private void ScaleCanvas(SKCanvas canvas)
+    {
+        float x = (float)TargetSize.X;
+        float y = (float)TargetSize.Y;
+
+        if (Stretch == Stretch.Fill)
+        {
+            canvas.Scale((float)Bounds.Width / x, (float)Bounds.Height / y);
+        }
+        else if (Stretch == Stretch.Uniform)
+        {
+            float scaleX = (float)Bounds.Width / x;
+            float scaleY = (float)Bounds.Height / y;
+            var scale = Math.Min(scaleX, scaleY);
+            float dX = (float)Bounds.Width / 2 / scale - x / 2;
+            float dY = (float)Bounds.Height / 2 / scale - y / 2;
+            canvas.Scale(scale, scale);
+            canvas.Translate(dX, dY);
+        }
+        else if (Stretch == Stretch.UniformToFill)
+        {
+            float scaleX = (float)Bounds.Width / x;
+            float scaleY = (float)Bounds.Height / y;
+            var scale = Math.Max(scaleX, scaleY);
+            float dX = (float)Bounds.Width / 2 / scale - x / 2;
+            float dY = (float)Bounds.Height / 2 / scale - y / 2;
+            canvas.Scale(scale, scale);
+            canvas.Translate(dX, dY);
+        }
+    }
+
+    public override bool Equals(ICustomDrawOperation? other)
+    {
+        return false;
+    }
+}

+ 26 - 0
src/PixiEditor/Views/Visuals/TextureImage.cs

@@ -0,0 +1,26 @@
+using Avalonia;
+using Avalonia.Media;
+using ChunkyImageLib;
+using PixiEditor.DrawingApi.Core;
+
+namespace PixiEditor.Views.Visuals;
+
+public class TextureImage : IImage
+{
+    public Texture Texture { get; set; }
+    public Stretch Stretch { get; set; } = Stretch.Uniform;
+
+    public Size Size { get; }
+
+    public TextureImage(Texture texture)
+    {
+        Texture = texture;
+        Size = new Size(texture.Size.X, texture.Size.Y);
+    }
+
+    public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
+    {
+        Texture.Surface.Flush();
+        context.Custom(new DrawTextureOperation(destRect, Stretch, Texture));
+    }
+}

+ 3 - 3
src/PixiEditor/Views/Windows/HelloTherePopup.axaml

@@ -129,8 +129,8 @@
                                                     CommandParameter="{Binding FilePath}"
                                                     x:Name="fileButton">
                                                 <Grid Width="100" Height="100">
-                                                    <visuals:SurfaceControl
-                                                        Surface="{Binding PreviewBitmap}"
+                                                    <visuals:TextureControl
+                                                        Texture="{Binding PreviewBitmap}"
                                                         Margin="10"
                                                         Stretch="Uniform"
                                                         x:Name="image">
@@ -141,7 +141,7 @@
                                                                 <Binding ElementName="image" Path="Width" />
                                                             </MultiBinding>
                                                         </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                                                    </visuals:SurfaceControl>
+                                                    </visuals:TextureControl>
                                                     <Border Grid.Row="1" Height="8" Width="8" x:Name="extensionBorder"
                                                             Margin="5"
                                                             Background="{Binding FileExtension, Converter={converters:FileExtensionToColorConverter}}"