瀏覽代碼

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

# Conflicts:
#	src/PixiEditor/Views/MainWindow.axaml.cs
CPKreuz 1 年之前
父節點
當前提交
84852f496a
共有 75 個文件被更改,包括 1518 次插入474 次删除
  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 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 string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
     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";
         string path = "ThirdParty/{0}/ffmpeg";
 #if WINDOWS
 #if WINDOWS
@@ -64,7 +64,9 @@ public class FFMpegRenderer : IAnimationRenderer
                 });
                 });
 
 
             var outputArgs = GetProcessorForFormat(args, outputPath, paletteTempPath);
             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())
             if (RequiresPaletteGeneration())
             {
             {
@@ -120,7 +122,9 @@ public class FFMpegRenderer : IAnimationRenderer
             .OutputToFile(outputPath, true, options =>
             .OutputToFile(outputPath, true, options =>
             {
             {
                 options.WithFramerate(FrameRate)
                 options.WithFramerate(FrameRate)
-                    .WithConstantRateFactor(21)
+                    .WithConstantRateFactor(18)
+                    .WithVideoBitrate(1800)
+                    .WithVideoCodec("mpeg4")
                     .ForcePixelFormat("yuv420p");
                     .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.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Context;
@@ -10,6 +11,7 @@ public class FuncContext
     public VecD Position { get; private set; }
     public VecD Position { get; private set; }
     public VecI Size { get; private set; }
     public VecI Size { get; private set; }
     public bool HasContext { get; private set; }
     public bool HasContext { get; private set; }
+    public RenderingContext RenderingContext { get; set; }
 
 
     public void ThrowOnMissingContext()
     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)
     public void UpdateContext(VecD position, VecI size)
     {
     {
         Position = position;
         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.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -13,8 +14,6 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 public class ModifyImageLeftNode : Node
 public class ModifyImageLeftNode : Node
 {
 {
-    private Pixmap? pixmap;
-
     public InputProperty<Surface?> Image { get; }
     public InputProperty<Surface?> Image { get; }
     
     
     public FuncOutputProperty<VecD> Coordinate { get; }
     public FuncOutputProperty<VecD> Coordinate { get; }
@@ -22,6 +21,8 @@ public class ModifyImageLeftNode : Node
     public FuncOutputProperty<Color> Color { get; }
     public FuncOutputProperty<Color> Color { get; }
 
 
     public override string DisplayName { get; set; } = "MODIFY_IMAGE_LEFT_NODE";
     public override string DisplayName { get; set; } = "MODIFY_IMAGE_LEFT_NODE";
+    
+    private ConcurrentDictionary<RenderingContext, Pixmap> pixmapCache = new();
 
 
     public ModifyImageLeftNode()
     public ModifyImageLeftNode()
     {
     {
@@ -33,19 +34,29 @@ public class ModifyImageLeftNode : Node
     private Color GetColor(FuncContext context)
     private Color GetColor(FuncContext context)
     {
     {
         context.ThrowOnMissingContext();
         context.ThrowOnMissingContext();
+
+        var targetPixmap = pixmapCache[context.RenderingContext];
         
         
-        if (pixmap == null)
+        if (targetPixmap == null)
             return new Color();
             return new Color();
         
         
         var x = context.Position.X * context.Size.X;
         var x = context.Position.X * context.Size.X;
         var y = context.Position.Y * context.Size.Y;
         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)
     protected override Surface? OnExecute(RenderingContext context)
@@ -53,6 +64,5 @@ public class ModifyImageLeftNode : Node
         return Image.Value;
         return Image.Value;
     }
     }
 
 
-
     public override Node CreateCopy() => new ModifyImageLeftNode();
     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";
     public override string DisplayName { get; set; } = "MODIFY_IMAGE_RIGHT_NODE";
 
 
+    private Surface surface;
+
     public ModifyImageRightNode()
     public ModifyImageRightNode()
     {
     {
         Coordinate = CreateFuncInput(nameof(Coordinate), "UV", new VecD());
         Coordinate = CreateFuncInput(nameof(Coordinate), "UV", new VecD());
@@ -49,34 +51,60 @@ public class ModifyImageRightNode : Node, IPairNodeEnd
         {
         {
             return null;
             return null;
         }
         }
-
-        startNode.PreparePixmap();
-
+        
         var width = size.X;
         var width = size.X;
         var height = size.Y;
         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()
     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,
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
         RectI? globalClippingRect = null)
         RectI? globalClippingRect = null)
     {
     {
-        using RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
+        RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
         try
         try
         {
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
@@ -60,6 +60,8 @@ public class DocumentRenderer
             }
             }
 
 
             using var chunkSnapshot = evaluated.DrawingSurface.Snapshot((RectI)sourceRect);
             using var chunkSnapshot = evaluated.DrawingSurface.Snapshot((RectI)sourceRect);
+            
+            if(context.IsDisposed) return new EmptyChunk();
 
 
             chunk.Surface.DrawingSurface.Canvas.DrawImage(chunkSnapshot, 0, 0, context.ReplacingPaintWithOpacity);
             chunk.Surface.DrawingSurface.Canvas.DrawImage(chunkSnapshot, 0, 0, context.ReplacingPaintWithOpacity);
 
 
@@ -71,6 +73,10 @@ public class DocumentRenderer
         {
         {
             return new EmptyChunk();
             return new EmptyChunk();
         }
         }
+        finally
+        {
+            context.Dispose();
+        }
     }
     }
 
 
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution,
     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 ChunkResolution ChunkResolution { get; }
     public VecI DocumentSize { get; set; }
     public VecI DocumentSize { get; set; }
 
 
+    public bool IsDisposed { get; private set; }
+    
     public RenderingContext(KeyFrameTime frameTime, VecI chunkToUpdate, ChunkResolution chunkResolution, VecI docSize)
     public RenderingContext(KeyFrameTime frameTime, VecI chunkToUpdate, ChunkResolution chunkResolution, VecI docSize)
     {
     {
         FrameTime = frameTime;
         FrameTime = frameTime;
@@ -55,6 +57,12 @@ public class RenderingContext : IDisposable
 
 
     public void Dispose()
     public void Dispose()
     {
     {
+        if (IsDisposed)
+        {
+            return;
+        }
+        
+        IsDisposed = true;
         BlendModePaint.Dispose();
         BlendModePaint.Dispose();
         BlendModeOpacityPaint.Dispose();
         BlendModeOpacityPaint.Dispose();
         ReplacingPaintWithOpacity.Dispose();
         ReplacingPaintWithOpacity.Dispose();

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

@@ -18,7 +18,9 @@ public class Program
             .UsePlatformDetect()
             .UsePlatformDetect()
             .With(new Win32PlatformOptions()
             .With(new Win32PlatformOptions()
             {
             {
-                RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Vulkan, Win32RenderingMode.AngleEgl }
+                RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Wgl, Win32RenderingMode.AngleEgl },
+                OverlayPopups = true
             })
             })
+           
             .LogToTrace();
             .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;
 using PixiEditor.DrawingApi.Core.Bridge.Operations;
 
 
 namespace PixiEditor.DrawingApi.Core.Bridge
 namespace PixiEditor.DrawingApi.Core.Bridge
@@ -20,5 +21,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge
         public IColorFilterImplementation ColorFilterImplementation { get; }
         public IColorFilterImplementation ColorFilterImplementation { get; }
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
         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 VecI GetSize(IntPtr objectPointer);
     public byte[] GetBytes(IntPtr objectPointer);
     public byte[] GetBytes(IntPtr objectPointer);
     public ImageInfo GetInfo(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 System;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
@@ -17,4 +16,6 @@ public interface ISurfaceImplementation
     public DrawingSurface Create(ImageInfo imageInfo);
     public DrawingSurface Create(ImageInfo imageInfo);
     public void Dispose(DrawingSurface drawingSurface);
     public void Dispose(DrawingSurface drawingSurface);
     public object GetNativeSurface(IntPtr objectPointer);
     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);
         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);
             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 />
     /// <value />
     public readonly RectI Rect => RectI.Create(this.Width, this.Height);
     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);
     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);
         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)
     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]
         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;
             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 class SkiaBitmapImplementation : SkObjectImplementation<SKBitmap>, IBitmapImplementation
     {
     {
         public SkiaImageImplementation ImageImplementation { get; }
         public SkiaImageImplementation ImageImplementation { get; }
-        public SkiaBitmapImplementation(SkiaImageImplementation imgImpl)
+        
+        private readonly SkiaPixmapImplementation _pixmapImplementation;
+
+        public SkiaBitmapImplementation(SkiaImageImplementation imgImpl, SkiaPixmapImplementation pixmapImplementation)
         {
         {
             ImageImplementation = imgImpl;
             ImageImplementation = imgImpl;
+            _pixmapImplementation = pixmapImplementation;
         }
         }
 
 
         public void Dispose(IntPtr objectPointer)
         public void Dispose(IntPtr objectPointer)
         {
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
             SKBitmap bitmap = ManagedInstances[objectPointer];
-            bitmap.Dispose();   
-            
+            bitmap.Dispose();
+
             ManagedInstances.TryRemove(objectPointer, out _);
             ManagedInstances.TryRemove(objectPointer, out _);
         }
         }
 
 
@@ -37,7 +41,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             ManagedInstances[skBitmap.Handle] = skBitmap;
             ManagedInstances[skBitmap.Handle] = skBitmap;
             return new Bitmap(skBitmap.Handle);
             return new Bitmap(skBitmap.Handle);
         }
         }
-        
+
         public VecI GetSize(IntPtr objectPointer)
         public VecI GetSize(IntPtr objectPointer)
         {
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
             SKBitmap bitmap = ManagedInstances[objectPointer];
@@ -47,15 +51,22 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public byte[] GetBytes(IntPtr objectPointer)
         public byte[] GetBytes(IntPtr objectPointer)
         {
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
             SKBitmap bitmap = ManagedInstances[objectPointer];
-            return bitmap.Bytes; 
+            return bitmap.Bytes;
         }
         }
-        
+
         public ImageInfo GetInfo(IntPtr objectPointer)
         public ImageInfo GetInfo(IntPtr objectPointer)
         {
         {
             SKBitmap bitmap = ManagedInstances[objectPointer];
             SKBitmap bitmap = ManagedInstances[objectPointer];
             return bitmap.Info.ToImageInfo();
             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)
         public object GetNativeBitmap(IntPtr objectPointer)
         {
         {
             return ManagedInstances[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)
         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)
         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;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace PixiEditor.DrawingApi.Skia.Implementations
 namespace PixiEditor.DrawingApi.Skia.Implementations
@@ -14,20 +15,32 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         private readonly SkiaCanvasImplementation _canvasImplementation;
         private readonly SkiaCanvasImplementation _canvasImplementation;
         private readonly SkiaPaintImplementation _paintImplementation;
         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;
             _pixmapImplementation = pixmapImplementation;
             _canvasImplementation = canvasImplementation;
             _canvasImplementation = canvasImplementation;
             _paintImplementation = paintImplementation;
             _paintImplementation = paintImplementation;
+            GrContext = context;
         }
         }
-        
+
         public Pixmap PeekPixels(DrawingSurface drawingSurface)
         public Pixmap PeekPixels(DrawingSurface drawingSurface)
         {
         {
             SKPixmap pixmap = ManagedInstances[drawingSurface.ObjectPointer].PeekPixels();
             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);
             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)
             int srcY)
         {
         {
             return ManagedInstances[drawingSurface.ObjectPointer]
             return ManagedInstances[drawingSurface.ObjectPointer]
@@ -38,34 +51,85 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
         {
             SKCanvas canvas = _canvasImplementation[surfaceToDraw.ObjectPointer];
             SKCanvas canvas = _canvasImplementation[surfaceToDraw.ObjectPointer];
             SKPaint paint = _paintImplementation[drawingPaint.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)
         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);
             return CreateDrawingSurface(skSurface);
         }
         }
 
 
         public DrawingSurface Create(ImageInfo imageInfo, IntPtr pixelBuffer)
         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);
             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)
         public DrawingSurface Create(Pixmap pixmap)
         {
         {
             SKPixmap skPixmap = _pixmapImplementation[pixmap.ObjectPointer];
             SKPixmap skPixmap = _pixmapImplementation[pixmap.ObjectPointer];
-            SKSurface skSurface = SKSurface.Create(skPixmap);
+            var skSurface = CreateSkiaSurface(skPixmap);
+
             return CreateDrawingSurface(skSurface);
             return CreateDrawingSurface(skSurface);
         }
         }
 
 
+        private SKSurface CreateSkiaSurface(SKPixmap skPixmap)
+        {
+            SKSurface skSurface = SKSurface.Create(skPixmap); 
+            return skSurface;
+        }
+
         public DrawingSurface Create(ImageInfo imageInfo)
         public DrawingSurface Create(ImageInfo imageInfo)
         {
         {
-            SKSurface skSurface = SKSurface.Create(imageInfo.ToSkImageInfo());
+            SKSurface skSurface = CreateSkiaSurface(imageInfo.ToSkImageInfo(), imageInfo.GpuBacked);
             return CreateDrawingSurface(skSurface);
             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)
         public void Dispose(DrawingSurface drawingSurface)
         {
         {
             ManagedInstances[drawingSurface.ObjectPointer].Dispose();
             ManagedInstances[drawingSurface.ObjectPointer].Dispose();
@@ -84,7 +148,13 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
 
             DrawingSurface surface = new DrawingSurface(skSurface.Handle, canvas);
             DrawingSurface surface = new DrawingSurface(skSurface.Handle, canvas);
             ManagedInstances[skSurface.Handle] = skSurface;
             ManagedInstances[skSurface.Handle] = skSurface;
+
             return surface;
             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;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Bridge.Operations;
 using PixiEditor.DrawingApi.Core.Bridge.Operations;
+using PixiEditor.DrawingApi.Skia.Exceptions;
 using PixiEditor.DrawingApi.Skia.Implementations;
 using PixiEditor.DrawingApi.Skia.Implementations;
+using SkiaSharp;
 
 
 namespace PixiEditor.DrawingApi.Skia
 namespace PixiEditor.DrawingApi.Skia
 {
 {
     public class SkiaDrawingBackend : IDrawingBackend
     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 IColorImplementation ColorImplementation { get; }
         public IImageImplementation ImageImplementation { get; }
         public IImageImplementation ImageImplementation { get; }
         public IImgDataImplementation ImgDataImplementation { get; }
         public IImgDataImplementation ImgDataImplementation { get; }
@@ -15,13 +33,16 @@ namespace PixiEditor.DrawingApi.Skia
         public IVectorPathImplementation PathImplementation { get; }
         public IVectorPathImplementation PathImplementation { get; }
         public IMatrix3X3Implementation MatrixImplementation { get; }
         public IMatrix3X3Implementation MatrixImplementation { get; }
         public IPixmapImplementation PixmapImplementation { get; }
         public IPixmapImplementation PixmapImplementation { get; }
-        public ISurfaceImplementation SurfaceImplementation { get; }
+        ISurfaceImplementation IDrawingBackend.SurfaceImplementation => SurfaceImplementation;
+        public SkiaSurfaceImplementation SurfaceImplementation { get; }
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
         public IColorFilterImplementation ColorFilterImplementation { get; }
         public IColorFilterImplementation ColorFilterImplementation { get; }
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
         public IShaderImplementation ShaderImplementation { get; set; }
 
 
+        private GRContext _grContext;
+
         public SkiaDrawingBackend()
         public SkiaDrawingBackend()
         {
         {
             ColorImplementation = new SkiaColorImplementation();
             ColorImplementation = new SkiaColorImplementation();
@@ -54,26 +75,22 @@ namespace PixiEditor.DrawingApi.Skia
             
             
             SkiaImageImplementation imgImpl = new SkiaImageImplementation(dataImpl, pixmapImpl);
             SkiaImageImplementation imgImpl = new SkiaImageImplementation(dataImpl, pixmapImpl);
             ImageImplementation = imgImpl;
             ImageImplementation = imgImpl;
-
-            SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl);
+            SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl, pixmapImpl);
             BitmapImplementation = bitmapImpl;
             BitmapImplementation = bitmapImpl;
             
             
             SkiaCanvasImplementation canvasImpl = new SkiaCanvasImplementation(paintImpl, imgImpl, bitmapImpl, pathImpl);
             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;
             CanvasImplementation = canvasImpl;
-
-            SurfaceImplementation = surfaceImpl;
-
         }
         }
         
         
         public void Setup()
         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"
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:converters="using:Avalonia.Controls.Converters">
                     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>
 </ResourceDictionary>

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

@@ -687,5 +687,12 @@
   "LERP_NODE": "Lerp",
   "LERP_NODE": "Lerp",
   "FROM": "From",
   "FROM": "From",
   "TO": "To",
   "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);
         return Enum.GetValues(EnumType);
     }
     }

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

@@ -345,11 +345,11 @@ internal class DocumentUpdater
     {
     {
         VecI oldSize = doc.SizeBindable;
         VecI oldSize = doc.SizeBindable;
 
 
-        foreach ((ChunkResolution res, Surface surf) in doc.Surfaces)
+        foreach ((ChunkResolution res, Texture surf) in doc.Surfaces)
         {
         {
             surf.Dispose();
             surf.Dispose();
             VecI size = (VecI)(info.Size * res.Multiplier());
             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);
         doc.SetSize(info.Size);
@@ -358,7 +358,7 @@ internal class DocumentUpdater
 
 
         VecI documentPreviewSize = StructureHelpers.CalculatePreviewSize(info.Size);
         VecI documentPreviewSize = StructureHelpers.CalculatePreviewSize(info.Size);
         doc.PreviewSurface.Dispose();
         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
         // TODO: Make sure property changed events are raised internally
         // UPDATE: I think I did, but I'll leave it commented out for now
         // 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;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO.FileEncoders;
 using PixiEditor.Models.IO.FileEncoders;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
@@ -18,17 +19,19 @@ internal abstract class ImageFileType : IoFileType
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Image;
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Image;
 
 
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
-        ExportConfig exportConfig)
+        ExportConfig exportConfig, ExportJob? job)
     {
     {
         Surface finalSurface;
         Surface finalSurface;
         if (exportConfig.ExportAsSpriteSheet)
         if (exportConfig.ExportAsSpriteSheet)
         {
         {
-            finalSurface = GenerateSpriteSheet(document, exportConfig);
+            job?.Report(0, new LocalizedString("GENERATING_SPRITE_SHEET"));
+            finalSurface = GenerateSpriteSheet(document, exportConfig, job);
             if (finalSurface == null)
             if (finalSurface == null)
                 return SaveResult.UnknownError;
                 return SaveResult.UnknownError;
         }
         }
         else
         else
         {
         {
+            job?.Report(0, new LocalizedString("RENDERING_IMAGE")); 
             var maybeBitmap = document.TryRenderWholeImage(0);
             var maybeBitmap = document.TryRenderWholeImage(0);
             if (maybeBitmap.IsT0)
             if (maybeBitmap.IsT0)
                 return SaveResult.ConcurrencyError;
                 return SaveResult.ConcurrencyError;
@@ -51,11 +54,13 @@ internal abstract class ImageFileType : IoFileType
         UniversalFileEncoder encoder = new(mappedFormat);
         UniversalFileEncoder encoder = new(mappedFormat);
         var result = await TrySaveAs(encoder, pathWithExtension, finalSurface);
         var result = await TrySaveAs(encoder, pathWithExtension, finalSurface);
         finalSurface.Dispose();
         finalSurface.Dispose();
+        
+        job?.Report(1, new LocalizedString("FINISHED"));
 
 
         return result;
         return result;
     }
     }
 
 
-    private Surface? GenerateSpriteSheet(DocumentViewModel document, ExportConfig config)
+    private Surface? GenerateSpriteSheet(DocumentViewModel document, ExportConfig config, ExportJob? job)
     {
     {
         if (document is null)
         if (document is null)
             return null;
             return null;
@@ -66,21 +71,27 @@ internal abstract class ImageFileType : IoFileType
         columns = Math.Max(1, columns);
         columns = Math.Max(1, columns);
 
 
         Surface surface = new Surface(new VecI(config.ExportSize.X * columns, config.ExportSize.Y * rows));
         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 x = index % columns;
             int y = index / columns;
             int y = index / columns;
             Surface target = frame;
             Surface target = frame;
             if (config.ExportSize != frame.Size)
             if (config.ExportSize != frame.Size)
             {
             {
-               target =
+                target =
                     frame.ResizeNearestNeighbor(new VecI(config.ExportSize.X, config.ExportSize.Y));
                     frame.ResizeNearestNeighbor(new VecI(config.ExportSize.X, config.ExportSize.Y));
             }
             }
             
             
             surface!.DrawingSurface.Canvas.DrawSurface(target.DrawingSurface, x * config.ExportSize.X, y * config.ExportSize.Y);
             surface!.DrawingSurface.Canvas.DrawSurface(target.DrawingSurface, x * config.ExportSize.X, y * config.ExportSize.Y);
             target.Dispose();
             target.Dispose();
-        });
+        }, job?.CancellationTokenSource.Token ?? CancellationToken.None);
 
 
         return surface;
         return surface;
     }
     }

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

@@ -45,5 +45,5 @@ internal abstract class IoFileType
         return "*" + extension;
         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 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
         try
         {
         {
+            job?.Report(0, "Serializing document");
             await Parser.PixiParser.V5.SerializeAsync(document.ToSerializable(), pathWithExtension);
             await Parser.PixiParser.V5.SerializeAsync(document.ToSerializable(), pathWithExtension);
+            job?.Report(1, "Document serialized");
         }
         }
         catch (UnauthorizedAccessException e)
         catch (UnauthorizedAccessException e)
         {
         {

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

@@ -1,5 +1,6 @@
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 
 
@@ -10,15 +11,23 @@ internal abstract class VideoFileType : IoFileType
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Video;
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Video;
 
 
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
-        ExportConfig config)
+        ExportConfig config, ExportJob? job)
     {
     {
         if (config.AnimationRenderer is null)
         if (config.AnimationRenderer is null)
             return SaveResult.UnknownError;
             return SaveResult.UnknownError;
 
 
         List<Image> frames = new(); 
         List<Image> frames = new(); 
+        
+        job?.Report(0, new LocalizedString("WARMING_UP"));
+        
+        int frameRendered = 0;
+        int totalFrames = document.AnimationDataViewModel.FramesCount;
 
 
         document.RenderFrames(frames, surface =>
         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)
             if (config.ExportSize != surface.Size)
             {
             {
                 return surface.ResizeNearestNeighbor(config.ExportSize);
                 return surface.ResizeNearestNeighbor(config.ExportSize);
@@ -26,8 +35,15 @@ internal abstract class VideoFileType : IoFileType
 
 
             return surface;
             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)
         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 IAnimationHandler AnimationHandler { get; }
     public VectorPath SelectionPathBindable { get; }
     public VectorPath SelectionPathBindable { get; }
     public INodeGraphHandler NodeGraphHandler { get; }
     public INodeGraphHandler NodeGraphHandler { get; }
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; }
+    public Dictionary<ChunkResolution, Texture> Surfaces { get; set; }
     public DocumentStructureModule StructureHelper { get; }
     public DocumentStructureModule StructureHelper { get; }
-    public Surface PreviewSurface { get; set; }
+    public Texture PreviewSurface { get; set; }
     public bool AllChangesSaved { get; }
     public bool AllChangesSaved { get; }
     public string CoordinatesString { get; set; }
     public string CoordinatesString { get; set; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }

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

@@ -5,7 +5,7 @@ namespace PixiEditor.Models.Handlers;
 
 
 internal interface IKeyFrameHandler
 internal interface IKeyFrameHandler
 {
 {
-    public Surface? PreviewSurface { get; set; }
+    public Texture? PreviewSurface { get; set; }
     public int StartFrameBindable { get; }
     public int StartFrameBindable { get; }
     public int DurationBindable { get; }
     public int DurationBindable { get; }
     public bool IsSelected { get; set; }
     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 string InternalName { get; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
-    public Surface ResultPreview { get; set; }
+    public Texture? ResultPreview { get; set; }
     public VecD PositionBindable { get; set; }
     public VecD PositionBindable { get; set; }
     public bool IsSelected { get; set; }
     public bool IsSelected { get; set; }
     public void TraverseBackwards(Func<INodeHandler, bool> func);
     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 interface IReferenceLayerHandler : IHandler
 {
 {
-    public Surface? ReferenceBitmap { get; }
+    public Texture? ReferenceBitmap { get; }
     public ShapeCorners ReferenceShapeBindable { get; set; }
     public ShapeCorners ReferenceShapeBindable { get; set; }
     public bool IsTopMost { get; set; }
     public bool IsTopMost { get; set; }
     public bool IsTransforming { 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
 internal interface IStructureMemberHandler : INodeHandler
 {
 {
     public bool HasMaskBindable { get; }
     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 bool MaskIsVisibleBindable { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public float OpacityBindable { 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>
     /// <summary>
     /// Attempts to save file using a SaveFileDialog
     /// Attempts to save file using a SaveFileDialog
     /// </summary>
     /// </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);
         ExporterResult result = new(DialogSaveResult.UnknownError, null);
 
 
@@ -70,7 +70,7 @@ internal class Exporter
 
 
             var fileType = SupportedFilesHelper.GetSaveFileType(FileTypeDialogDataSet.SetKind.Any, file);
             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)
             if (saveResult.Result == SaveResult.Success)
             {
             {
                 result.Path = saveResult.finalPath;
                 result.Path = saveResult.finalPath;
@@ -85,10 +85,10 @@ internal class Exporter
     /// <summary>
     /// <summary>
     /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// </summary>
     /// </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);
         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)
         if (saveResult != SaveResult.Success)
             finalPath = "";
             finalPath = "";
 
 
@@ -98,7 +98,7 @@ internal class Exporter
     /// <summary>
     /// <summary>
     /// Attempts to save the document into the given location, filetype is inferred from path
     /// Attempts to save the document into the given location, filetype is inferred from path
     /// </summary>
     /// </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);
         string directory = Path.GetDirectoryName(pathWithExtension);
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
@@ -109,7 +109,9 @@ internal class Exporter
         if (typeFromPath is null)
         if (typeFromPath is null)
             return SaveResult.UnknownError;
             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)
     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>
     /// <returns>WriteableBitmap of imported image.</returns>
     public static Surface? ImportImage(string path, VecI size)
     public static Surface? ImportImage(string path, VecI size)
     {
     {
-        if (!Path.Exists(path)) 
+        if (!Path.Exists(path))
             throw new MissingFileException();
             throw new MissingFileException();
-        
+
         Surface original;
         Surface original;
         try
         try
         {
         {
@@ -45,7 +45,7 @@ internal class Importer : ObservableObject
         {
         {
             throw new CorruptedFileException(e);
             throw new CorruptedFileException(e);
         }
         }
-        
+
         if (original.Size == size || size == VecI.NegativeOne)
         if (original.Size == size || size == VecI.NegativeOne)
         {
         {
             return original;
             return original;
@@ -64,7 +64,8 @@ internal class Importer : ObservableObject
         }
         }
         catch (NotSupportedException e)
         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
         /*catch (FileFormatException e) TODO: Not found in Avalonia
         {
         {
@@ -126,7 +127,7 @@ internal class Importer : ObservableObject
                 // TODO: Handle
                 // TODO: Handle
                 throw new RecoverableException();
                 throw new RecoverableException();
             }
             }
-            
+
             var pixiDocument = parser.Deserialize(file);
             var pixiDocument = parser.Deserialize(file);
 
 
             var document = pixiDocument switch
             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))
         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")
         if (Path.GetExtension(path) != ".pixi")
             return Surface.Load(path);
             return Surface.Load(path);
 
 

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

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
@@ -196,7 +197,7 @@ internal class CanvasUpdater
                 globalScaledClippingRectangle =
                 globalScaledClippingRectangle =
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
 
 
-            Surface screenSurface = doc.Surfaces[resolution];
+            Texture screenSurface = doc.Surfaces[resolution];
             foreach (var chunkPos in chunks)
             foreach (var chunkPos in chunks)
             {
             {
                 RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle,
                 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)
         RectI? globalClippingRectangle, RectI? globalScaledClippingRectangle)
     {
     {
         if (screenSurface is null || screenSurface.IsDisposed)
         if (screenSurface is null || screenSurface.IsDisposed)
             return;
             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)
         doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime, globalClippingRectangle)
             .Switch(
             .Switch(
                 (Chunk chunk) =>
                 (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 _) =>
                 (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.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
@@ -12,6 +13,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
@@ -191,7 +193,7 @@ internal class MemberPreviewUpdater
             if (member is null)
             if (member is null)
                 continue;
                 continue;
 
 
-            if (forMasks && member.Mask.Value is null)
+            if (forMasks && member.Mask.NonOverridenValue is null)
             {
             {
                 newPreviewBitmapSizes.Add(guid, null);
                 newPreviewBitmapSizes.Add(guid, null);
                 continue;
                 continue;
@@ -237,17 +239,14 @@ internal class MemberPreviewUpdater
                 if (member.PreviewSurface is not null && member.PreviewSurface.Size.X == newSize.Value.previewSize.X &&
                 if (member.PreviewSurface is not null && member.PreviewSurface.Size.X == newSize.Value.previewSize.X &&
                     member.PreviewSurface.Size.Y == newSize.Value.previewSize.Y)
                     member.PreviewSurface.Size.Y == newSize.Value.previewSize.Y)
                 {
                 {
-                    member.PreviewSurface!.DrawingSurface.Canvas.Clear();
+                    member.PreviewSurface!.Surface.Canvas.Clear();
                 }
                 }
                 else
                 else
                 {
                 {
                     member.PreviewSurface?.Dispose();
                     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
         // update masks
@@ -262,11 +261,8 @@ internal class MemberPreviewUpdater
             }
             }
             else
             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,
     private RectI? GetOrFindMemberTightBounds(IReadOnlyStructureNode member, int atFrame,
         AffectedArea currentlyAffectedArea, bool forMask)
         AffectedArea currentlyAffectedArea, bool forMask)
     {
     {
-        if (forMask && member.Mask.Value is null)
+        if (forMask && member.Mask.NonOverridenValue is null)
             throw new InvalidOperationException();
             throw new InvalidOperationException();
 
 
         RectI? prevTightBounds = null;
         RectI? prevTightBounds = null;
@@ -308,10 +304,10 @@ internal class MemberPreviewUpdater
     /// </summary>
     /// </summary>
     private RectI? FindLayerTightBounds(IReadOnlyLayerNode layer, int frame, bool forMask)
     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();
             throw new InvalidOperationException();
 
 
-        if (layer.Mask.Value is not null && forMask)
+        if (layer.Mask.NonOverridenValue is not null && forMask)
             return FindImageTightBoundsFast(layer.Mask.Value);
             return FindImageTightBoundsFast(layer.Mask.Value);
 
 
         if (layer is IReadOnlyImageNode raster)
         if (layer is IReadOnlyImageNode raster)
@@ -440,22 +436,22 @@ internal class MemberPreviewUpdater
             };
             };
             var pos = chunkPos * resolution.PixelSize();
             var pos = chunkPos * resolution.PixelSize();
             var rendered = doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime);
             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)
             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);
                     resolution.PixelSize(), ClearPaint);
             }
             }
             else if (rendered.IsT0)
             else if (rendered.IsT0)
             {
             {
                 using var renderedChunk = rendered.AsT0;
                 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)
         if (somethingChanged)
@@ -543,14 +539,14 @@ internal class MemberPreviewUpdater
         IReadOnlyStructureNode member, [DisallowNull] AffectedArea? affArea, VecI position, float scaling)
         IReadOnlyStructureNode member, [DisallowNull] AffectedArea? affArea, VecI position, float scaling)
     {
     {
         bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
         bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
-        if(!isEditingRootImage)
+        if (!isEditingRootImage)
             return;
             return;
-        
+
         if (keyFrame.PreviewSurface == null ||
         if (keyFrame.PreviewSurface == null ||
             keyFrame.PreviewSurface.Size != memberVM.PreviewSurface.Size)
             keyFrame.PreviewSurface.Size != memberVM.PreviewSurface.Size)
         {
         {
             keyFrame.PreviewSurface?.Dispose();
             keyFrame.PreviewSurface?.Dispose();
-            keyFrame.PreviewSurface = new Surface(memberVM.PreviewSurface.Size);
+            keyFrame.PreviewSurface = new Texture(memberVM.PreviewSurface.Size);
         }
         }
 
 
         RenderLayerMainPreview((IReadOnlyLayerNode)member, keyFrame.PreviewSurface, affArea.Value,
         RenderLayerMainPreview((IReadOnlyLayerNode)member, keyFrame.PreviewSurface, affArea.Value,
@@ -564,73 +560,79 @@ internal class MemberPreviewUpdater
         AffectedArea area,
         AffectedArea area,
         VecI position, float scaling)
         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>
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
     /// </summary>
     /// </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)
         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)
     private void RenderAnimationFramePreview(IReadOnlyImageNode node, IKeyFrameHandler keyFrameVM, AffectedArea area)
@@ -638,24 +640,27 @@ internal class MemberPreviewUpdater
         if (keyFrameVM.PreviewSurface is null)
         if (keyFrameVM.PreviewSurface is null)
         {
         {
             keyFrameVM.PreviewSurface =
             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(
     private void RenderMaskPreviews(
@@ -701,19 +706,23 @@ internal class MemberPreviewUpdater
 
 
             var member = internals.Tracker.Document.FindMemberOrThrow(guid);
             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));
             infos.Add(new MaskPreviewDirty_RenderInfo(guid));
         }
         }
     }
     }
@@ -739,22 +748,38 @@ internal class MemberPreviewUpdater
             if (nodeVm.ResultPreview == null)
             if (nodeVm.ResultPreview == null)
             {
             {
                 nodeVm.ResultPreview =
                 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 scalingX = (float)nodeVm.ResultPreview.Size.X / node.CachedResult.Size.X;
             float scalingY = (float)nodeVm.ResultPreview.Size.Y / node.CachedResult.Size.Y;
             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));
             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 string filePath;
 
 
-    private Surface previewBitmap;
+    private Texture previewBitmap;
 
 
     public string FilePath
     public string FilePath
     {
     {
@@ -61,7 +61,7 @@ internal class RecentlyOpenedDocument : ObservableObject
         }
         }
     }
     }
 
 
-    public Surface PreviewBitmap
+    public Texture PreviewBitmap
     {
     {
         get
         get
         {
         {
@@ -80,7 +80,7 @@ internal class RecentlyOpenedDocument : ObservableObject
         FilePath = path;
         FilePath = path;
     }
     }
 
 
-    private Surface? LoadPreviewBitmap()
+    private Texture? LoadPreviewBitmap()
     {
     {
         if (!File.Exists(FilePath))
         if (!File.Exists(FilePath))
         {
         {
@@ -91,7 +91,7 @@ internal class RecentlyOpenedDocument : ObservableObject
         {
         {
             try
             try
             {
             {
-                return Importer.GetPreviewBitmap(FilePath);
+                return Importer.GetPreviewTexture(FilePath);
             }
             }
             catch
             catch
             {
             {
@@ -101,11 +101,11 @@ internal class RecentlyOpenedDocument : ObservableObject
 
 
         if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
         if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
         {
         {
-            Surface bitmap = null;
+            Texture bitmap = null;
 
 
             try
             try
             {
             {
-                bitmap = Surface.Load(FilePath);
+                bitmap = Texture.Load(FilePath);
             }
             }
             catch (RecoverableException)
             catch (RecoverableException)
             {
             {
@@ -122,12 +122,12 @@ internal class RecentlyOpenedDocument : ObservableObject
         return null;
         return null;
     }
     }
 
 
-    private Surface DownscaleToMaxSize(Surface bitmap)
+    private Texture DownscaleToMaxSize(Texture bitmap)
     {
     {
         if (bitmap.Size.X > Constants.MaxPreviewWidth || bitmap.Size.Y > Constants.MaxPreviewHeight)
         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);
             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);
                 ResizeMethod.HighQuality);
             return scaledBitmap;
             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:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters">
                     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
     <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">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
                 <Grid>
                 <Grid>
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
-                            Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 5"
+                            Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 15"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                         <Grid>
                         <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"
                             <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>
                         </Grid>
                     </Border>
                     </Border>
-                    
+
                     <Border IsVisible="{Binding !IsCollapsed, RelativeSource={RelativeSource TemplatedParent}}"
                     <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"
                             BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True" Name="PreviewBorder"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             RenderOptions.BitmapInterpolationMode="None">
                             RenderOptions.BitmapInterpolationMode="None">
@@ -33,8 +35,8 @@
                                 </ImageBrush.Transform>
                                 </ImageBrush.Transform>
                             </ImageBrush>
                             </ImageBrush>
                         </Border.Background>
                         </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">
                             Stretch="Uniform" Width="60" Height="60">
                             <ui:RenderOptionsBindable.BitmapInterpolationMode>
                             <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                 <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
                                 <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
@@ -43,29 +45,32 @@
                                     <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
                                     <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
                                 </MultiBinding>
                                 </MultiBinding>
                             </ui:RenderOptionsBindable.BitmapInterpolationMode>
                             </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                        </visuals:SurfaceControl>
+                        </visuals:TextureControl>
                     </Border>
                     </Border>
                 </Grid>
                 </Grid>
             </ControlTemplate>
             </ControlTemplate>
         </Setter>
         </Setter>
-        
+
         <Style Selector="^:collapsed">
         <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>
-        
+
         <Style Selector="^:collapsed /template/ Panel#PART_ResizePanelLeft">
         <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>
-        
+
         <Style Selector="^:selected /template/ Border#MainBorder">
         <Style Selector="^:selected /template/ Border#MainBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>
         </Style>
-        
+
         <Style Selector="^:selected /template/ Border#PreviewBorder">
         <Style Selector="^:selected /template/ Border#PreviewBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>
         </Style>
-        
+
         <Style Selector="^:disabled">
         <Style Selector="^:disabled">
             <Setter Property="Opacity" Value="0.5" />
             <Setter Property="Opacity" Value="0.5" />
         </Style>
         </Style>

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

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

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

@@ -23,8 +23,8 @@
                                     </ImageBrush.Transform>
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                                 </ImageBrush>
                             </Border.Background>
                             </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">
                                 Stretch="Uniform" Width="60" Height="60">
                                 <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                 <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
@@ -33,7 +33,7 @@
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
                                     </MultiBinding>
                                     </MultiBinding>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                            </visuals:SurfaceControl>
+                            </visuals:TextureControl>
                         </Border>
                         </Border>
                         <TextBlock Margin="5 0 0 0" VerticalAlignment="Center" Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
                         <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" />
                         <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(),
                         Id = "DocumentArea", FallbackContent = new CreateDocumentFallbackView(),
                         Dockables = [ DockContext.CreateDockable(nodeGraphDockViewModel) ]
                         Dockables = [ DockContext.CreateDockable(nodeGraphDockViewModel) ]
                     },
                     },
-                    FirstSize = 0.85,
+                    SecondSize = 200,
                     SplitDirection = DockingDirection.Bottom,
                     SplitDirection = DockingDirection.Bottom,
                     Second = new DockableArea
                     Second = new DockableArea
                     {
                     {
@@ -71,7 +71,7 @@ internal class LayoutManager
                         ActiveDockable = DockContext.CreateDockable(timelineDockViewModel)
                         ActiveDockable = DockContext.CreateDockable(timelineDockViewModel)
                     }
                     }
                 },
                 },
-                FirstSize = 0.85,
+                SecondSize = 360,
                 SplitDirection = DockingDirection.Right,
                 SplitDirection = DockingDirection.Right,
                 Second = new DockableTree
                 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;
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
 
 
     //TODO: It was DrawingSurface before, check if it's correct
     //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;
         get => previewSurface;
         set
         set
@@ -237,7 +237,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
 
 
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
         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);
         ReferenceLayerViewModel = new(this, Internals);
 
 
@@ -626,7 +626,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
     public Color? PickColorFromReferenceLayer(VecD pos)
     public Color? PickColorFromReferenceLayer(VecD pos)
     {
     {
-        Surface? bitmap = ReferenceLayerViewModel.ReferenceBitmap;
+        Texture? bitmap = ReferenceLayerViewModel.ReferenceBitmap;
         if (bitmap is null)
         if (bitmap is null)
             return 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)
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return [];
             return [];
+        
+        if(token.IsCancellationRequested)
+            return [];
 
 
         int firstFrame = AnimationDataViewModel.FirstFrame;
         int firstFrame = AnimationDataViewModel.FirstFrame;
         int framesCount = AnimationDataViewModel.FramesCount;
         int framesCount = AnimationDataViewModel.FramesCount;
         int lastFrame = firstFrame + framesCount;
         int lastFrame = firstFrame + framesCount;
 
 
         Image[] images = new Image[framesCount];
         Image[] images = new Image[framesCount];
+        
+        // TODO: Multi-threading
         for (int i = firstFrame; i < lastFrame; i++)
         for (int i = firstFrame; i < lastFrame; i++)
         {
         {
+            if (token.IsCancellationRequested)
+                return [];
+            
             double normalizedTime = (double)(i - firstFrame) / framesCount;
             double normalizedTime = (double)(i - firstFrame) / framesCount;
             KeyFrameTime frameTime = new KeyFrameTime(i, normalizedTime);
             KeyFrameTime frameTime = new KeyFrameTime(i, normalizedTime);
             var surface = TryRenderWholeImage(frameTime);
             var surface = TryRenderWholeImage(frameTime);
@@ -863,7 +871,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     ///     Render frames progressively and disposes the surface after processing.
     ///     Render frames progressively and disposes the surface after processing.
     /// </summary>
     /// </summary>
     /// <param name="processFrameAction">Action to perform on rendered frame</param>
     /// <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)
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return;
             return;
@@ -876,7 +885,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
         for (int i = firstFrame; i < lastFrame; i++)
         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)
             if (surface.IsT0)
             {
             {
                 continue;
                 continue;

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

@@ -9,7 +9,7 @@ namespace PixiEditor.ViewModels.Document;
 
 
 internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
 internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
 {
 {
-    private Surface? previewSurface;
+    private Texture? previewSurface;
     private int startFrameBindable;
     private int startFrameBindable;
     private int durationBindable;
     private int durationBindable;
     private bool isVisibleBindable = true;
     private bool isVisibleBindable = true;
@@ -27,7 +27,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
 
 
     IDocument IKeyFrameHandler.Document => Document;
     IDocument IKeyFrameHandler.Document => Document;
 
 
-    public Surface? PreviewSurface
+    public Texture? PreviewSurface
     {
     {
         get => previewSurface;
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);
         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 const double TopMostOpacity = 0.6;
     
     
-    public Surface? ReferenceBitmap { get; private set; }
+    public Texture? ReferenceBitmap { get; private set; }
 
 
     private ShapeCorners referenceShape;
     private ShapeCorners referenceShape;
     public ShapeCorners ReferenceShapeBindable 
     public ShapeCorners ReferenceShapeBindable 
@@ -114,7 +114,7 @@ internal class ReferenceLayerViewModel : ObservableObject, IReferenceLayerHandle
     
     
     public void SetReferenceLayer(ImmutableArray<byte> imageBgra8888Bytes, VecI imageSize, ShapeCorners shape)
     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;
         referenceShape = shape;
         isVisible = true;
         isVisible = true;
         isTransforming = false;
         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.ChangeableDocument.Actions.Generated;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
@@ -143,16 +144,16 @@ internal abstract class StructureMemberViewModel : NodeViewModel, IStructureMemb
         set => SetProperty(ref selection, value);
         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;
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);
         set => SetProperty(ref previewSurface, value);
     }
     }
 
 
-    public Surface? MaskPreviewSurface
+    public Texture? MaskPreviewSurface
     {
     {
         get => maskPreviewSurface;
         get => maskPreviewSurface;
         set => SetProperty(ref maskPreviewSurface, value);
         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 VecD position;
     private ObservableRangeCollection<INodePropertyHandler> inputs = new();
     private ObservableRangeCollection<INodePropertyHandler> inputs = new();
     private ObservableRangeCollection<INodePropertyHandler> outputs = new();
     private ObservableRangeCollection<INodePropertyHandler> outputs = new();
-    private Surface resultPreview;
+    private Texture resultPreview;
     private bool isSelected;
     private bool isSelected;
 
 
     protected Guid id;
     protected Guid id;
@@ -71,7 +71,7 @@ internal class NodeViewModel : ObservableObject, INodeHandler
         set => SetProperty(ref outputs, value);
         set => SetProperty(ref outputs, value);
     }
     }
 
 
-    public Surface ResultPreview
+    public Texture ResultPreview
     {
     {
         get => resultPreview;
         get => resultPreview;
         set => SetProperty(ref resultPreview, value);
         set => SetProperty(ref resultPreview, value);

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

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

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

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
@@ -8,6 +9,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using Newtonsoft.Json.Linq;
 using Newtonsoft.Json.Linq;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
@@ -362,7 +364,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         string finalPath = null;
         string finalPath = null;
         if (asNew || string.IsNullOrEmpty(document.FullFilePath))
         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)
             if (result.Result == DialogSaveResult.Cancelled)
                 return false;
                 return false;
             if (result.Result != DialogSaveResult.Success)
             if (result.Result != DialogSaveResult.Success)
@@ -376,7 +378,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
         else
         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)
             if (result != SaveResult.Success)
             {
             {
                 ShowSaveError((DialogSaveResult)result);
                 ShowSaveError((DialogSaveResult)result);
@@ -412,12 +414,23 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             };
             };
             if (await info.ShowDialog())
             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)
         catch (RecoverableException e)

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

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

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

@@ -192,7 +192,7 @@ internal partial class ExportFilePopup : PixiEditorPopup
         videoPreviewTimer.Stop();
         videoPreviewTimer.Stop();
         videoPreviewTimer.Tick -= OnVideoPreviewTimerOnTick;
         videoPreviewTimer.Tick -= OnVideoPreviewTimerOnTick;
         videoPreviewTimer = null;
         videoPreviewTimer = null;
-        cancellationTokenSource.Dispose();
+        cancellationTokenSource.Cancel();
 
 
         ExportPreview?.Dispose();
         ExportPreview?.Dispose();
 
 
@@ -210,6 +210,11 @@ internal partial class ExportFilePopup : PixiEditorPopup
         if (videoPreviewFrames.Length > 0)
         if (videoPreviewFrames.Length > 0)
         {
         {
             ExportPreview.DrawingSurface.Canvas.Clear();
             ExportPreview.DrawingSurface.Canvas.Clear();
+            if (videoPreviewFrames[activeFrame] == null)
+            {
+                return;
+            }
+            
             ExportPreview.DrawingSurface.Canvas.DrawImage(videoPreviewFrames[activeFrame], 0, 0);
             ExportPreview.DrawingSurface.Canvas.DrawImage(videoPreviewFrames[activeFrame], 0, 0);
             activeFrame = (activeFrame + 1) % videoPreviewFrames.Length;
             activeFrame = (activeFrame + 1) % videoPreviewFrames.Length;
         }
         }
@@ -232,7 +237,8 @@ internal partial class ExportFilePopup : PixiEditorPopup
         if (IsVideoExport)
         if (IsVideoExport)
         {
         {
             StartRenderAnimationJob();
             StartRenderAnimationJob();
-            videoPreviewTimer.Interval = TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRateBindable);
+            videoPreviewTimer.Interval =
+                TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRateBindable);
         }
         }
         else
         else
         {
         {
@@ -242,43 +248,72 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
 
     private void RenderImagePreview()
     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()
     private void GenerateSpriteSheetPreview()
     {
     {
         int clampedColumns = Math.Max(SpriteSheetColumns, 1);
         int clampedColumns = Math.Max(SpriteSheetColumns, 1);
         int clampedRows = Math.Max(SpriteSheetRows, 1);
         int clampedRows = Math.Max(SpriteSheetRows, 1);
-        
+
         VecI previewSize = CalculatePreviewSize(new VecI(SaveWidth * clampedColumns, SaveHeight * clampedRows));
         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)
         if (previewSize != ExportPreview.Size)
         {
         {
             ExportPreview?.Dispose();
             ExportPreview?.Dispose();
             ExportPreview = new Surface(previewSize);
             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(
         Task.Run(
             () =>
             () =>
             {
             {
-                videoPreviewFrames = document.RenderFrames(ProcessFrame);
+                videoPreviewFrames = document.RenderFrames(ProcessFrame, cancellationTokenSource.Token);
             }, cancellationTokenSource.Token).ContinueWith(_ =>
             }, cancellationTokenSource.Token).ContinueWith(_ =>
         {
         {
             Dispatcher.UIThread.Invoke(() =>
             Dispatcher.UIThread.Invoke(() =>
@@ -429,7 +464,10 @@ internal partial class ExportFilePopup : PixiEditorPopup
     {
     {
         if (e.Sender is ExportFilePopup popup)
         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.Transform>
                                 </ImageBrush>
                                 </ImageBrush>
                             </Border.Background>
                             </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>
                                 <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
                                         <Binding Path="Folder.PreviewSurface.Size.X" ElementName="folderControl"/>
                                         <Binding Path="Folder.PreviewSurface.Size.X" ElementName="folderControl"/>
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width"/>
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width"/>
                                     </MultiBinding>
                                     </MultiBinding>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                            </visuals:SurfaceControl>
+                            </visuals:TextureControl>
                         </Border>
                         </Border>
                         <Border 
                         <Border 
                             Width="32" Height="32" 
                             Width="32" Height="32" 
@@ -89,7 +90,7 @@
                                 </ImageBrush>
                                 </ImageBrush>
                             </Border.Background>
                             </Border.Background>
                             <Grid IsHitTestVisible="False">
                             <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"/>
                                     RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                                 <Path 
                                 <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" 
                                 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"/>
                                 <Binding ElementName="uc" Path="Layer.HasMaskBindable"/>
                             </MultiBinding>
                             </MultiBinding>
                         </Border.BorderBrush>
                         </Border.BorderBrush>
-                        <visuals:SurfaceControl Surface="{Binding Layer.PreviewSurface, ElementName=uc}"
+                        <visuals:TextureControl Texture="{Binding Layer.PreviewSurface, ElementName=uc}"
                                                 Stretch="Uniform" Width="30"
                                                 Stretch="Uniform" Width="30"
                                                 Height="30"
                                                 Height="30"
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
@@ -100,7 +100,7 @@
                             </MultiBinding>
                             </MultiBinding>
                         </Border.BorderBrush>
                         </Border.BorderBrush>
                         <Grid IsHitTestVisible="False">
                         <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"/>
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                             <Path
                             <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"
                                 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"
                             BorderBrush="Black"
                             Background="{DynamicResource ThemeBackgroundBrush}"
                             Background="{DynamicResource ThemeBackgroundBrush}"
                             Margin="5, 0, 10, 0">
                             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"
                                    Stretch="Uniform" Width="26" Height="26"
                                    RenderOptions.BitmapInterpolationMode="HighQuality" IsHitTestVisible="False" />
                                    RenderOptions.BitmapInterpolationMode="HighQuality" IsHitTestVisible="False" />
                         </Border>
                         </Border>

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

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

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

@@ -14,10 +14,10 @@
              VerticalAlignment="Center"
              VerticalAlignment="Center"
              d:DesignHeight="450" d:DesignWidth="800">
              d:DesignHeight="450" d:DesignWidth="800">
 
 
-    <visuals:SurfaceControl
+    <visuals:TextureControl
         x:Name="mainImage"
         x:Name="mainImage"
         Focusable="True"
         Focusable="True"
-        Surface="{Binding TargetBitmap, ElementName=uc}"
+        Texture="{Binding TargetBitmap, ElementName=uc}"
         Stretch="Uniform"
         Stretch="Uniform"
         SizeChanged="OnImageSizeChanged">
         SizeChanged="OnImageSizeChanged">
         <ui1:RenderOptionsBindable.BitmapInterpolationMode>
         <ui1:RenderOptionsBindable.BitmapInterpolationMode>
@@ -26,5 +26,5 @@
                 <Binding ElementName="mainImage" Path="Bounds.Width"/>
                 <Binding ElementName="mainImage" Path="Bounds.Width"/>
             </MultiBinding>
             </MultiBinding>
         </ui1:RenderOptionsBindable.BitmapInterpolationMode>
         </ui1:RenderOptionsBindable.BitmapInterpolationMode>
-    </visuals:SurfaceControl>
+    </visuals:TextureControl>
 </UserControl>
 </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;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
-using Avalonia.Media.Imaging;
-using ChunkyImageLib;
+using Avalonia.Media;
+using Avalonia.Threading;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
@@ -21,8 +19,8 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
         AvaloniaProperty.Register<FixedViewport, DocumentViewModel>(nameof(Document), null);
         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 =
     public static readonly StyledProperty<bool> DelayedProperty =
         AvaloniaProperty.Register<FixedViewport, bool>(nameof(Delayed), false);
         AvaloniaProperty.Register<FixedViewport, bool>(nameof(Delayed), false);
@@ -35,7 +33,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DelayedProperty, value);
         set => SetValue(DelayedProperty, value);
     }
     }
 
 
-    public Dictionary<ChunkResolution, WriteableBitmap>? Bitmaps
+    public Dictionary<ChunkResolution, Texture>? Bitmaps
     {
     {
         get => GetValue(BitmapsProperty);
         get => GetValue(BitmapsProperty);
         set => SetValue(BitmapsProperty, value);
         set => SetValue(BitmapsProperty, value);
@@ -47,11 +45,11 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DocumentProperty, value);
         set => SetValue(DocumentProperty, value);
     }
     }
 
 
-    public Surface? TargetBitmap
+    public Texture? TargetBitmap
     {
     {
         get
         get
         {
         {
-            if (Document?.Surfaces.TryGetValue(CalculateResolution(), out Surface? value) == true)
+            if (Document?.Surfaces.TryGetValue(CalculateResolution(), out Texture? value) == true)
                 return value;
                 return value;
             return null;
             return null;
         }
         }
@@ -146,7 +144,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         Document?.Operations.AddOrUpdateViewport(GetLocation());
         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;
         FixedViewport? viewport = (FixedViewport)args.Sender;
         viewport.PropertyChanged?.Invoke(viewport, new(nameof(TargetBitmap)));
         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
         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;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
+using Avalonia.OpenGL;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 using Avalonia.Threading;
+using Avalonia.Vulkan;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Skia;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.DrawingApi.Skia.Implementations;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
@@ -31,10 +36,15 @@ internal partial class MainWindow : Window
 
 
     private StartupPerformance _startupPerformance = new();
     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)
             if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
                 return desktop.MainWindow as MainWindow;
                 return desktop.MainWindow as MainWindow;
@@ -60,6 +70,7 @@ internal partial class MainWindow : Window
         AsyncImageLoader.ImageLoader.AsyncImageLoader = new DiskCachedWebImageLoader();
         AsyncImageLoader.ImageLoader.AsyncImageLoader = new DiskCachedWebImageLoader();
 
 
         SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
         SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
+        skiaDrawingBackend.GraphicsContext = GetOpenGlGrContext();
         DrawingBackendApi.SetupBackend(skiaDrawingBackend);
         DrawingBackendApi.SetupBackend(skiaDrawingBackend);
 
 
         preferences = services.GetRequiredService<IPreferences>();
         preferences = services.GetRequiredService<IPreferences>();
@@ -74,6 +85,31 @@ internal partial class MainWindow : Window
         InitializeComponent();
         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)
     public static MainWindow CreateWithRecoveredDocuments(CrashReport report, out bool showMissingFilesDialog)
     {
     {
         var window = GetMainWindow();
         var window = GetMainWindow();

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

@@ -34,7 +34,7 @@ public class NodeView : TemplatedControl
         AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
         AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
             nameof(Outputs));
             nameof(Outputs));
 
 
-    public static readonly StyledProperty<Surface> ResultPreviewProperty = AvaloniaProperty.Register<NodeView, Surface>(
+    public static readonly StyledProperty<Texture> ResultPreviewProperty = AvaloniaProperty.Register<NodeView, Texture>(
         nameof(ResultPreview));
         nameof(ResultPreview));
 
 
     public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<NodeView, bool>(
     public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<NodeView, bool>(
@@ -64,7 +64,7 @@ public class NodeView : TemplatedControl
         set => SetValue(IsSelectedProperty, value);
         set => SetValue(IsSelectedProperty, value);
     }
     }
 
 
-    public Surface ResultPreview
+    public Texture ResultPreview
     {
     {
         get => GetValue(ResultPreviewProperty);
         get => GetValue(ResultPreviewProperty);
         set => SetValue(ResultPreviewProperty, value);
         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.Media;
 using Avalonia.Styling;
 using Avalonia.Styling;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Helpers.Converters;
 using PixiEditor.Helpers.Converters;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.Views.Visuals;
 using PixiEditor.Views.Visuals;
+using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 
 
 namespace PixiEditor.Views.Overlays;
 namespace PixiEditor.Views.Overlays;
 
 
 internal class ReferenceLayerOverlay : Overlay
 internal class ReferenceLayerOverlay : Overlay
 {
 {
     private const float OpacityTransitionDuration = 0.1f;
     private const float OpacityTransitionDuration = 0.1f;
+
     public static readonly StyledProperty<ReferenceLayerViewModel> ReferenceLayerProperty =
     public static readonly StyledProperty<ReferenceLayerViewModel> ReferenceLayerProperty =
         AvaloniaProperty.Register<ReferenceLayerOverlay, ReferenceLayerViewModel>(
         AvaloniaProperty.Register<ReferenceLayerOverlay, ReferenceLayerViewModel>(
             nameof(ReferenceLayerViewModel));
             nameof(ReferenceLayerViewModel));
@@ -59,6 +64,12 @@ internal class ReferenceLayerOverlay : Overlay
         : OverlayRenderSorting.Background;
         : OverlayRenderSorting.Background;
 
 
     private Pen borderBen = new Pen(Brushes.Black, 2);
     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()
     static ReferenceLayerOverlay()
     {
     {
@@ -79,8 +90,16 @@ internal class ReferenceLayerOverlay : Overlay
 
 
             RectD dirty = new RectD(0, 0, ReferenceLayer.ReferenceBitmap.Size.X, ReferenceLayer.ReferenceBitmap.Size.Y);
             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);
             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);
             context.Custom(drawOperation);
 
 
             matrix.Dispose();
             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);
             float length = (float)Math.Sqrt(movedDistance.X * movedDistance.X + movedDistance.Y * movedDistance.Y);
             if (length > 10)
             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;
                 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.RemoveAt(currIndex);
                     Colors.Insert(newIndex, paletteColor);
                     Colors.Insert(newIndex, paletteColor);
                     int indexOfSource = Colors.IndexOf(paletteColorControl.Color);
                     int indexOfSource = Colors.IndexOf(paletteColorControl.Color);
+                    if(indexOfSource < 0) return;
                     Colors.Move(indexOfSource, currIndex);
                     Colors.Move(indexOfSource, currIndex);
                 }
                 }
             }
             }

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

@@ -13,8 +13,10 @@ using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Skia;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.DrawingApi.Skia.Extensions;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Converters;
 using PixiEditor.Helpers.Converters;
@@ -32,7 +34,7 @@ namespace PixiEditor.Views.Rendering;
 
 
 internal class Scene : Zoombox.Zoombox, ICustomHitTest
 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));
         nameof(Surface));
 
 
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
@@ -86,7 +88,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(DocumentProperty, value);
         set => SetValue(DocumentProperty, value);
     }
     }
 
 
-    public Surface Surface
+    public Texture Surface
     {
     {
         get => GetValue(SurfaceProperty);
         get => GetValue(SurfaceProperty);
         set => SetValue(SurfaceProperty, value);
         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);
         RectD dirtyBounds = new RectD(0, 0, Document.Width / resolutionScale, Document.Height / resolutionScale);
         Rect dirtyRect = new Rect(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,
         using var operation = new DrawSceneOperation(Surface, Document, CanvasPos, Scale * resolutionScale, angle,
             FlipX, FlipY,
             FlipX, FlipY,
             dirtyRect,
             dirtyRect,
@@ -185,9 +188,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                 }
                 }
 
 
                 overlay.ZoomScale = Scale;
                 overlay.ZoomScale = Scale;
-                
-                if(!overlay.CanRender()) continue;
-                
+
+                if (!overlay.CanRender()) continue;
+
                 overlay.RenderOverlay(context, dirtyBounds);
                 overlay.RenderOverlay(context, dirtyBounds);
                 Cursor = overlay.Cursor ?? DefaultCursor;
                 Cursor = overlay.Cursor ?? DefaultCursor;
             }
             }
@@ -453,7 +456,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
 
     private static void SurfaceChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
     private static void SurfaceChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
     {
     {
-        if (e.NewValue is Surface surface)
+        if (e.NewValue is Texture surface)
         {
         {
             scene.ContentDimensions = surface.Size;
             scene.ContentDimensions = surface.Size;
         }
         }
@@ -475,7 +478,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
 
 internal class DrawSceneOperation : SkiaDrawOperation
 internal class DrawSceneOperation : SkiaDrawOperation
 {
 {
-    public Surface Surface { get; set; }
+    public Texture Surface { get; set; }
     public DocumentViewModel Document { get; set; }
     public DocumentViewModel Document { get; set; }
     public VecD ContentPosition { get; set; }
     public VecD ContentPosition { get; set; }
     public double Scale { get; set; }
     public double Scale { get; set; }
@@ -489,7 +492,9 @@ internal class DrawSceneOperation : SkiaDrawOperation
 
 
     private SKPaint _paint = new SKPaint();
     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,
         double angle, bool flipX, bool flipY, Rect dirtyBounds, Rect viewportBounds, double opacity,
         ColorMatrix colorMatrix) : base(dirtyBounds)
         ColorMatrix colorMatrix) : base(dirtyBounds)
     {
     {
@@ -520,13 +525,25 @@ internal class DrawSceneOperation : SkiaDrawOperation
             return;
             return;
         }
         }
 
 
-        using Image snapshot = Surface.DrawingSurface.Snapshot(SurfaceRectToRender);
+        using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);
+
 
 
         var matrixValues = new float[ColorMatrix.Width * ColorMatrix.Height];
         var matrixValues = new float[ColorMatrix.Width * ColorMatrix.Height];
         ColorMatrix.TryGetMembers(matrixValues);
         ColorMatrix.TryGetMembers(matrixValues);
 
 
         _paint.ColorFilter = SKColorFilter.CreateColorMatrix(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();
         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}"
                                                     CommandParameter="{Binding FilePath}"
                                                     x:Name="fileButton">
                                                     x:Name="fileButton">
                                                 <Grid Width="100" Height="100">
                                                 <Grid Width="100" Height="100">
-                                                    <visuals:SurfaceControl
-                                                        Surface="{Binding PreviewBitmap}"
+                                                    <visuals:TextureControl
+                                                        Texture="{Binding PreviewBitmap}"
                                                         Margin="10"
                                                         Margin="10"
                                                         Stretch="Uniform"
                                                         Stretch="Uniform"
                                                         x:Name="image">
                                                         x:Name="image">
@@ -141,7 +141,7 @@
                                                                 <Binding ElementName="image" Path="Width" />
                                                                 <Binding ElementName="image" Path="Width" />
                                                             </MultiBinding>
                                                             </MultiBinding>
                                                         </ui:RenderOptionsBindable.BitmapInterpolationMode>
                                                         </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                                                    </visuals:SurfaceControl>
+                                                    </visuals:TextureControl>
                                                     <Border Grid.Row="1" Height="8" Width="8" x:Name="extensionBorder"
                                                     <Border Grid.Row="1" Height="8" Width="8" x:Name="extensionBorder"
                                                             Margin="5"
                                                             Margin="5"
                                                             Background="{Binding FileExtension, Converter={converters:FileExtensionToColorConverter}}"
                                                             Background="{Binding FileExtension, Converter={converters:FileExtensionToColorConverter}}"