Browse Source

Merge branch 'direct-rendering' into FullGpuDrawingBackend

flabbet 11 months ago
parent
commit
fd9b353ad4
38 changed files with 752 additions and 377 deletions
  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. 21 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  5. 49 21
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  6. 23 17
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  7. 8 0
      src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs
  8. 1 0
      src/PixiEditor.DrawingApi.Core/Bridge/Operations/ISurfaceImplementation.cs
  9. 5 0
      src/PixiEditor.DrawingApi.Core/Surfaces/DrawingSurface.cs
  10. 19 66
      src/PixiEditor.DrawingApi.Core/Texture.cs
  11. 1 1
      src/PixiEditor.DrawingApi.Skia/Implementations/SKObjectImplementation.cs
  12. 16 2
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs
  13. 10 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaSurfaceImplementation.cs
  14. 123 109
      src/PixiEditor.UI.Common/Controls/ProgressBar.axaml
  15. 8 1
      src/PixiEditor/Data/Localization/Languages/en.json
  16. 1 1
      src/PixiEditor/Helpers/MarkupExtensions/EnumExtension.cs
  17. 16 9
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  18. 17 6
      src/PixiEditor/Models/Files/ImageFileType.cs
  19. 1 1
      src/PixiEditor/Models/Files/IoFileType.cs
  20. 3 1
      src/PixiEditor/Models/Files/PixiFileType.cs
  21. 19 3
      src/PixiEditor/Models/Files/VideoFileType.cs
  22. 29 0
      src/PixiEditor/Models/IO/ExportJob.cs
  23. 8 6
      src/PixiEditor/Models/IO/Exporter.cs
  24. 63 34
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  25. 6 6
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  26. 21 16
      src/PixiEditor/Styles/Templates/KeyFrame.axaml
  27. 2 2
      src/PixiEditor/ViewModels/Dock/LayoutManager.cs
  28. 17 3
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  29. 21 8
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  30. 62 24
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs
  31. 48 0
      src/PixiEditor/Views/Dialogs/ProgressDialog.cs
  32. 17 0
      src/PixiEditor/Views/Dialogs/ProgressPopup.axaml
  33. 51 0
      src/PixiEditor/Views/Dialogs/ProgressPopup.axaml.cs
  34. 3 1
      src/PixiEditor/Views/Main/Tools/ToolPickerButton.axaml
  35. 2 2
      src/PixiEditor/Views/Nodes/NodeView.cs
  36. 30 11
      src/PixiEditor/Views/Rendering/Scene.cs
  37. 10 11
      src/PixiEditor/Views/Visuals/TextureControl.cs
  38. 1 0
      src/PixiEditor/Views/Visuals/TextureImage.cs

+ 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;

+ 21 - 11
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,19 +14,19 @@ 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<Texture?> Image { get; }
+    public InputProperty<Surface?> Image { get; }
     
     
     public FuncOutputProperty<VecD> Coordinate { get; }
     public FuncOutputProperty<VecD> Coordinate { get; }
     
     
     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()
     {
     {
-        Image = CreateInput<Texture>(nameof(Surface), "IMAGE", null);
+        Image = CreateInput<Surface>(nameof(Surface), "IMAGE", null);
         Coordinate = CreateFuncOutput(nameof(Coordinate), "UV", ctx => ctx.Position);
         Coordinate = CreateFuncOutput(nameof(Coordinate), "UV", ctx => ctx.Position);
         Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
         Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
     }
     }
@@ -33,26 +34,35 @@ 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?.PeekReadOnlyPixels();
+        pixmapCache[forContext] = Image.Value?.DrawingSurface.Snapshot().PeekPixels();
+    }
+    
+    internal void DisposePixmap(RenderingContext forContext)
+    {
+        if (pixmapCache.TryRemove(forContext, out var targetPixmap))
+        {
+            targetPixmap?.Dispose();
+        }
     }
     }
 
 
-    protected override Texture? OnExecute(RenderingContext context)
+    protected override Surface? OnExecute(RenderingContext context)
     {
     {
         return Image.Value;
         return Image.Value;
     }
     }
 
 
-
     public override Node CreateCopy() => new ModifyImageLeftNode();
     public override Node CreateCopy() => new ModifyImageLeftNode();
 }
 }

+ 49 - 21
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -22,18 +22,20 @@ public class ModifyImageRightNode : Node, IPairNodeEnd
     public FuncInputProperty<VecD> Coordinate { get; }
     public FuncInputProperty<VecD> Coordinate { get; }
     public FuncInputProperty<Color> Color { get; }
     public FuncInputProperty<Color> Color { get; }
 
 
-    public OutputProperty<Texture> Output { get; }
+    public OutputProperty<Surface> Output { get; }
 
 
     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());
         Color = CreateFuncInput(nameof(Color), "COLOR", new Color());
         Color = CreateFuncInput(nameof(Color), "COLOR", new Color());
-        Output = CreateOutput<Texture>(nameof(Output), "OUTPUT", null);
+        Output = CreateOutput<Surface>(nameof(Output), "OUTPUT", null);
     }
     }
 
 
-    protected override Texture? OnExecute(RenderingContext renderingContext)
+    protected override Surface? OnExecute(RenderingContext renderingContext)
     {
     {
         if (StartNode == null)
         if (StartNode == null)
         {
         {
@@ -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 Texture(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.Surface.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()

+ 23 - 17
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -20,12 +20,12 @@ 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);
 
 
-            Texture? evaluated = Document.NodeGraph.Execute(context);
+            Surface? evaluated = Document.NodeGraph.Execute(context);
             if (evaluated is null)
             if (evaluated is null)
             {
             {
                 return new EmptyChunk();
                 return new EmptyChunk();
@@ -33,12 +33,12 @@ public class DocumentRenderer
 
 
             Chunk chunk = Chunk.Create(resolution);
             Chunk chunk = Chunk.Create(resolution);
 
 
-            chunk.Surface.Surface.Canvas.Save();
-            chunk.Surface.Surface.Canvas.Clear();
+            chunk.Surface.DrawingSurface.Canvas.Save();
+            chunk.Surface.DrawingSurface.Canvas.Clear();
 
 
             if (transformedClippingRect is not null)
             if (transformedClippingRect is not null)
             {
             {
-                chunk.Surface.Surface.Canvas.ClipRect((RectD)transformedClippingRect);
+                chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
             }
             }
 
 
             VecD pos = chunkPos;
             VecD pos = chunkPos;
@@ -59,11 +59,13 @@ public class DocumentRenderer
                 return new EmptyChunk();
                 return new EmptyChunk();
             }
             }
 
 
-            using var chunkSnapshot = evaluated.Surface.Snapshot((RectI)sourceRect);
+            using var chunkSnapshot = evaluated.DrawingSurface.Snapshot((RectI)sourceRect);
+            
+            if(context.IsDisposed) return new EmptyChunk();
 
 
-            chunk.Surface.Surface.Canvas.DrawImage(chunkSnapshot, 0, 0, context.ReplacingPaintWithOpacity);
+            chunk.Surface.DrawingSurface.Canvas.DrawImage(chunkSnapshot, 0, 0, context.ReplacingPaintWithOpacity);
 
 
-            chunk.Surface.Surface.Canvas.Restore();
+            chunk.Surface.DrawingSurface.Canvas.Restore();
 
 
             return chunk;
             return chunk;
         }
         }
@@ -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,
@@ -81,7 +87,7 @@ public class DocumentRenderer
         {
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
 
 
-            Texture? evaluated = node.Execute(context);
+            Surface? evaluated = node.Execute(context);
             if (evaluated is null)
             if (evaluated is null)
             {
             {
                 return new EmptyChunk();
                 return new EmptyChunk();
@@ -112,7 +118,7 @@ public class DocumentRenderer
         NodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         NodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         try
         try
         {
         {
-            Texture? evaluated = membersOnlyGraph.Execute(context);
+            Surface? evaluated = membersOnlyGraph.Execute(context);
             if (evaluated is null)
             if (evaluated is null)
             {
             {
                 return new EmptyChunk();
                 return new EmptyChunk();
@@ -147,7 +153,7 @@ public class DocumentRenderer
             }
             }
         });
         });
 
 
-        IInputProperty<Texture> lastInput = outputNode.Input;
+        IInputProperty<Surface> lastInput = outputNode.Input;
 
 
         foreach (var layer in layersInOrder)
         foreach (var layer in layersInOrder)
         {
         {
@@ -162,28 +168,28 @@ public class DocumentRenderer
     }
     }
 
 
     private static OneOf<Chunk, EmptyChunk> ChunkFromResult(ChunkResolution resolution,
     private static OneOf<Chunk, EmptyChunk> ChunkFromResult(ChunkResolution resolution,
-        RectI? transformedClippingRect, Texture evaluated,
+        RectI? transformedClippingRect, Surface evaluated,
         RenderingContext context)
         RenderingContext context)
     {
     {
         Chunk chunk = Chunk.Create(resolution);
         Chunk chunk = Chunk.Create(resolution);
 
 
-        chunk.Surface.Surface.Canvas.Save();
-        chunk.Surface.Surface.Canvas.Clear();
+        chunk.Surface.DrawingSurface.Canvas.Save();
+        chunk.Surface.DrawingSurface.Canvas.Clear();
 
 
         int x = 0;
         int x = 0;
         int y = 0;
         int y = 0;
 
 
         if (transformedClippingRect is not null)
         if (transformedClippingRect is not null)
         {
         {
-            chunk.Surface.Surface.Canvas.ClipRect((RectD)transformedClippingRect);
+            chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
             x = transformedClippingRect.Value.X;
             x = transformedClippingRect.Value.X;
             y = transformedClippingRect.Value.Y;
             y = transformedClippingRect.Value.Y;
         }
         }
 
 
-        chunk.Surface.Surface.Canvas.DrawSurface(evaluated.Surface, x, y,
+        chunk.Surface.DrawingSurface.Canvas.DrawSurface(evaluated.DrawingSurface, x, y,
             context.ReplacingPaintWithOpacity);
             context.ReplacingPaintWithOpacity);
 
 
-        chunk.Surface.Surface.Canvas.Restore();
+        chunk.Surface.DrawingSurface.Canvas.Restore();
 
 
         return chunk;
         return chunk;
     }
     }

+ 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();

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

@@ -17,5 +17,6 @@ public interface ISurfaceImplementation
     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);
     public void Flush(DrawingSurface drawingSurface);
+    public DrawingSurface CreateFromNative(object native);
 }
 }
 
 

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

@@ -80,5 +80,10 @@ namespace PixiEditor.DrawingApi.Core.Surfaces
         {
         {
             DrawingBackendApi.Current.SurfaceImplementation.Flush(this);
             DrawingBackendApi.Current.SurfaceImplementation.Flush(this);
         }
         }
+
+        public static DrawingSurface CreateFromNative(object native)
+        {
+            return DrawingBackendApi.Current.SurfaceImplementation.CreateFromNative(native);
+        }
     }
     }
 }
 }

+ 19 - 66
src/PixiEditor.DrawingApi.Core/Texture.cs

@@ -11,7 +11,7 @@ namespace PixiEditor.DrawingApi.Core;
 public class Texture : IDisposable
 public class Texture : IDisposable
 {
 {
     public VecI Size { get; }
     public VecI Size { get; }
-    public DrawingSurface Surface { get; }
+    public DrawingSurface Surface { get; private set; }
 
 
     public event SurfaceChangedEventHandler? Changed;
     public event SurfaceChangedEventHandler? Changed;
 
 
@@ -20,9 +20,6 @@ public class Texture : IDisposable
     private bool pixmapUpToDate;
     private bool pixmapUpToDate;
     private Pixmap pixmap;
     private Pixmap pixmap;
 
 
-    private Paint nearestNeighborReplacingPaint =
-        new() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.None };
-
     public Texture(VecI size)
     public Texture(VecI size)
     {
     {
         Size = size;
         Size = size;
@@ -36,23 +33,19 @@ public class Texture : IDisposable
         Surface.Changed += SurfaceOnChanged;
         Surface.Changed += SurfaceOnChanged;
     }
     }
 
 
-    public Texture(Texture createFrom)
+    internal Texture(DrawingSurface surface)
     {
     {
-        Size = createFrom.Size;
-
-        Surface =
-            DrawingSurface.Create(
-                new ImageInfo(Size.X, Size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgb())
-                {
-                    GpuBacked = true
-                });
-
-        Surface.Canvas.DrawSurface(createFrom.Surface, 0, 0);
+        Surface = surface;
+        Surface.Changed += SurfaceOnChanged;
+    }
+    
+    ~Texture()
+    {
+       Surface.Changed -= SurfaceOnChanged;
     }
     }
 
 
     private void SurfaceOnChanged(RectD? changedRect)
     private void SurfaceOnChanged(RectD? changedRect)
     {
     {
-        pixmapUpToDate = false;
         Changed?.Invoke(changedRect);
         Changed?.Invoke(changedRect);
     }
     }
 
 
@@ -113,10 +106,10 @@ public class Texture : IDisposable
         return newTexture;
         return newTexture;
     }
     }
 
 
-    public Color GetSRGBPixel(VecI vecI)
+    public Color? GetSRGBPixel(VecI vecI)
     {
     {
         if (vecI.X < 0 || vecI.X >= Size.X || vecI.Y < 0 || vecI.Y >= Size.Y)
         if (vecI.X < 0 || vecI.X >= Size.X || vecI.Y < 0 || vecI.Y >= Size.Y)
-            return Color.Empty;
+            return null;
 
 
         if (!pixmapUpToDate)
         if (!pixmapUpToDate)
         {
         {
@@ -127,6 +120,11 @@ public class Texture : IDisposable
         return pixmap.GetPixelColor(vecI);
         return pixmap.GetPixelColor(vecI);
     }
     }
 
 
+    public void AddDirtyRect(RectI dirtyRect)
+    {
+        Changed?.Invoke(new RectD(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
+    }
+
     public void Dispose()
     public void Dispose()
     {
     {
         if (IsDisposed)
         if (IsDisposed)
@@ -137,54 +135,9 @@ public class Texture : IDisposable
         Surface.Dispose();
         Surface.Dispose();
     }
     }
 
 
-    public Pixmap? PeekReadOnlyPixels()
-    {
-        if (pixmapUpToDate)
-        {
-            return pixmap;
-        }
-
-        pixmap = Surface.PeekPixels();
-        pixmapUpToDate = true;
-
-        return pixmap;
-    }
-
-    public void CopyTo(Texture destination)
-    {
-        destination.Surface.Canvas.DrawSurface(Surface, 0, 0);
-    }
-
-    public unsafe bool IsFullyTransparent()
+    public static Texture FromExisting(DrawingSurface drawingSurface)
     {
     {
-        ulong* ptr = (ulong*)PeekReadOnlyPixels().GetPixels();
-        for (int i = 0; i < Size.X * Size.Y; i++)
-        {
-            // ptr[i] actually contains 4 16-bit floats. We only care about the first one which is alpha.
-            // An empty pixel can have alpha of 0 or -0 (not sure if -0 actually ever comes up). 0 in hex is 0x0, -0 in hex is 0x8000
-            if ((ptr[i] & 0x1111_0000_0000_0000) != 0 && (ptr[i] & 0x1111_0000_0000_0000) != 0x8000_0000_0000_0000)
-                return false;
-        }
-
-        return true;
-    }
-
-    public void DrawBytes(VecI surfaceSize, byte[] pixels, ColorType color, AlphaType alphaType)
-    {
-        if (surfaceSize != Size)
-            throw new ArgumentException("Surface size must match the size of the byte array");
-
-        using Image image = Image.FromPixels(new ImageInfo(Size.X, Size.Y, color, alphaType, ColorSpace.CreateSrgb()),
-            pixels);
-        Surface.Canvas.DrawImage(image, 0, 0);
-    }
-
-    public Texture ResizeNearestNeighbor(VecI newSize)
-    {
-        using Image image = Surface.Snapshot();
-        Texture newSurface = new(newSize);
-        newSurface.Surface.Canvas.DrawImage(image, new RectD(0, 0, newSize.X, newSize.Y),
-            nearestNeighborReplacingPaint);
-        return newSurface;
+        Texture texture = new(drawingSurface);
+        return texture;
     }
     }
 }
 }

+ 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;
         }
         }
     }
     }

+ 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)

+ 10 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaSurfaceImplementation.cs

@@ -156,5 +156,15 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
         {
             ManagedInstances[drawingSurface.ObjectPointer].Flush(true, true);
             ManagedInstances[drawingSurface.ObjectPointer].Flush(true, true);
         }
         }
+
+        public DrawingSurface CreateFromNative(object native)
+        {
+            if (native is not SKSurface skSurface)
+            {
+                throw new ArgumentException("Native object is not of type SKSurface");
+            }
+
+            return CreateDrawingSurface(skSurface);
+        }
     }
     }
 }
 }

+ 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);
     }
     }

+ 16 - 9
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -2,9 +2,11 @@
 using System.Linq;
 using System.Linq;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Threading;
 using Avalonia.Threading;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.DocumentPassthroughActions;
@@ -12,6 +14,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Rendering.RenderInfos;
 using PixiEditor.Models.Rendering.RenderInfos;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
+using PixiEditor.Views.Rendering;
 
 
 namespace PixiEditor.Models.DocumentModels;
 namespace PixiEditor.Models.DocumentModels;
 #nullable enable
 #nullable enable
@@ -33,6 +36,12 @@ internal class ActionAccumulator
 
 
         canvasUpdater = new(doc, internals);
         canvasUpdater = new(doc, internals);
         previewUpdater = new(doc, internals);
         previewUpdater = new(doc, internals);
+        Scene.Paint += SceneOnPaint;
+    }
+
+    private void SceneOnPaint(Texture obj)
+    {
+        canvasUpdater.Render(obj, ChunkResolution.Full); 
     }
     }
 
 
     public void AddFinishedActions(params IAction[] actions)
     public void AddFinishedActions(params IAction[] actions)
@@ -89,11 +98,10 @@ internal class ActionAccumulator
             if (undoBoundaryPassed)
             if (undoBoundaryPassed)
                 internals.Updater.AfterUndoBoundaryPassed();
                 internals.Updater.AfterUndoBoundaryPassed();
 
 
-            // update the contents of the bitmaps
             var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime, internals.Tracker, optimizedChanges);
             var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime, internals.Tracker, optimizedChanges);
             List<IRenderInfo> renderResult = new();
             List<IRenderInfo> renderResult = new();
-            renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest));
-            //renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
+            //await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest);
+            renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
 
 
             if (undoBoundaryPassed)
             if (undoBoundaryPassed)
             {
             {
@@ -139,7 +147,7 @@ internal class ActionAccumulator
                         RectI finalRect = new RectI(VecI.Zero, new(bitmap.Size.X, bitmap.Size.Y));
                         RectI finalRect = new RectI(VecI.Zero, new(bitmap.Size.X, bitmap.Size.Y));
 
 
                         RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
                         RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
-                        // bitmap.AddDirtyRect(dirtyRect);
+                        bitmap.AddDirtyRect(dirtyRect);
                     }
                     }
                     break;
                     break;
                 case PreviewDirty_RenderInfo info:
                 case PreviewDirty_RenderInfo info:
@@ -147,8 +155,7 @@ internal class ActionAccumulator
                         var bitmap = document.StructureHelper.Find(info.GuidValue)?.PreviewSurface;
                         var bitmap = document.StructureHelper.Find(info.GuidValue)?.PreviewSurface;
                         if (bitmap is null)
                         if (bitmap is null)
                             continue;
                             continue;
-                        //TODO: Implement dirty rects
-                        // bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
+                        bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
                     }
                     }
                     break;
                     break;
                 case MaskPreviewDirty_RenderInfo info:
                 case MaskPreviewDirty_RenderInfo info:
@@ -156,12 +163,12 @@ internal class ActionAccumulator
                         var bitmap = document.StructureHelper.Find(info.GuidValue)?.MaskPreviewSurface;
                         var bitmap = document.StructureHelper.Find(info.GuidValue)?.MaskPreviewSurface;
                         if (bitmap is null)
                         if (bitmap is null)
                             continue;
                             continue;
-                        //bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
+                        bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
                     }
                     }
                     break;
                     break;
                 case CanvasPreviewDirty_RenderInfo:
                 case CanvasPreviewDirty_RenderInfo:
                     {
                     {
-                        //document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X, document.PreviewSurface.Size.Y));
+                        document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X, document.PreviewSurface.Size.Y));
                     }
                     }
                     break;
                     break;
                 case NodePreviewDirty_RenderInfo info:
                 case NodePreviewDirty_RenderInfo info:
@@ -169,7 +176,7 @@ internal class ActionAccumulator
                         var node = document.StructureHelper.Find(info.NodeId);
                         var node = document.StructureHelper.Find(info.NodeId);
                         if (node is null || node.PreviewSurface is null)
                         if (node is null || node.PreviewSurface is null)
                             continue;
                             continue;
-                        //node.PreviewSurface.AddDirtyRect(new RectI(0, 0, node.PreviewSurface.Size.X, node.PreviewSurface.Size.Y));
+                        node.PreviewSurface.AddDirtyRect(new RectI(0, 0, node.PreviewSurface.Size.X, node.PreviewSurface.Size.Y));
                     }
                     }
                     break;
                     break;
             }
             }

+ 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)
         {
         {

+ 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)

+ 63 - 34
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -13,6 +13,7 @@ using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Rendering.RenderInfos;
 using PixiEditor.Models.Rendering.RenderInfos;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
+using PixiEditor.Views.Rendering;
 
 
 namespace PixiEditor.Models.Rendering;
 namespace PixiEditor.Models.Rendering;
 #nullable enable
 #nullable enable
@@ -21,7 +22,7 @@ internal class CanvasUpdater
     private readonly IDocument doc;
     private readonly IDocument doc;
     private readonly DocumentInternalParts internals;
     private readonly DocumentInternalParts internals;
 
 
-    private static readonly Paint ReplacingPaint = new() { BlendMode = BlendMode.Src };
+    private static readonly Paint ReplacingPaint = new() { BlendMode = BlendMode.SrcOver };
 
 
     private static readonly Paint ClearPaint = new()
     private static readonly Paint ClearPaint = new()
     {
     {
@@ -52,6 +53,15 @@ internal class CanvasUpdater
             [ChunkResolution.Eighth] = new()
             [ChunkResolution.Eighth] = new()
         };
         };
 
 
+    private Dictionary<ChunkResolution, HashSet<VecI>> nextRepaint =
+        new()
+        {
+            [ChunkResolution.Full] = new(),
+            [ChunkResolution.Half] = new(),
+            [ChunkResolution.Quarter] = new(),
+            [ChunkResolution.Eighth] = new()
+        };
+
 
 
     public CanvasUpdater(IDocument doc, DocumentInternalParts internals)
     public CanvasUpdater(IDocument doc, DocumentInternalParts internals)
     {
     {
@@ -62,19 +72,44 @@ internal class CanvasUpdater
     /// <summary>
     /// <summary>
     /// Don't call this outside ActionAccumulator
     /// Don't call this outside ActionAccumulator
     /// </summary>
     /// </summary>
-    public async Task<List<IRenderInfo>> UpdateGatheredChunks
+    public async Task UpdateGatheredChunks
         (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
         (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
     {
     {
-        return await Task.Run(() => Render(chunkGatherer, rerenderDelayed)).ConfigureAwait(true);
+        await Task.Run(() => QueueChunksToRender(chunkGatherer, rerenderDelayed)).ConfigureAwait(true);
     }
     }
 
 
     /// <summary>
     /// <summary>
     /// Don't call this outside ActionAccumulator
     /// Don't call this outside ActionAccumulator
     /// </summary>
     /// </summary>
-    public List<IRenderInfo> UpdateGatheredChunksSync
+    public void UpdateGatheredChunksSync
         (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
         (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
     {
     {
-        return Render(chunkGatherer, rerenderDelayed);
+        QueueChunksToRender(chunkGatherer, rerenderDelayed);
+    }
+
+    public void Render(Texture screenSurface, RectI? globalClippingRectangle)
+    {
+        UpdateMainImage(screenSurface, nextRepaint, globalClippingRectangle,
+            null);
+        
+        nextRepaint.Clear();
+    }
+
+    public void Render(Texture screenSurface, ChunkResolution resolution)
+    {
+        VecI chunks = new VecI(
+            (int)Math.Ceiling(doc.SizeBindable.X / (float)resolution.PixelSize()),
+            (int)Math.Ceiling(doc.SizeBindable.Y / (float)resolution.PixelSize()));
+        
+        RectI globalClippingRectangle = new RectI(new VecI(0, 0), doc.SizeBindable);
+        
+        for (int x = 0; x < chunks.X; x++)
+        {
+            for (int y = 0; y < chunks.Y; y++)
+            {
+                RenderChunk(new VecI(x, y), screenSurface, resolution, globalClippingRectangle, null);
+            }
+        }
     }
     }
 
 
     private Dictionary<ChunkResolution, HashSet<VecI>> FindChunksVisibleOnViewports(bool onDelayed, bool all)
     private Dictionary<ChunkResolution, HashSet<VecI>> FindChunksVisibleOnViewports(bool onDelayed, bool all)
@@ -152,7 +187,7 @@ internal class CanvasUpdater
         }
         }
     }
     }
 
 
-    private List<IRenderInfo> Render(AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
+    private void QueueChunksToRender(AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
     {
     {
         Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender =
         Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender =
             FindGlobalChunksToRerender(chunkGatherer, rerenderDelayed);
             FindGlobalChunksToRerender(chunkGatherer, rerenderDelayed);
@@ -178,15 +213,14 @@ internal class CanvasUpdater
         }
         }
 
 
         if (!anythingToUpdate)
         if (!anythingToUpdate)
-            return new();
+            return;
 
 
-        List<IRenderInfo> infos = new();
-        UpdateMainImage(chunksToRerender, updatingStoredChunks ? null : chunkGatherer.MainImageArea.GlobalArea.Value,
-            infos);
-        return infos;
+        nextRepaint = chunksToRerender;
     }
     }
 
 
-    private void UpdateMainImage(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender,
+    private void UpdateMainImage(
+        Texture screenSurface,
+        Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender,
         RectI? globalClippingRectangle, List<IRenderInfo> infos)
         RectI? globalClippingRectangle, List<IRenderInfo> infos)
     {
     {
         foreach (var (resolution, chunks) in chunksToRerender)
         foreach (var (resolution, chunks) in chunksToRerender)
@@ -197,25 +231,20 @@ internal class CanvasUpdater
                 globalScaledClippingRectangle =
                 globalScaledClippingRectangle =
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
 
 
-            Texture screenSurface = doc.Surfaces[resolution];
+            //Texture screenSurface = doc.Surfaces[resolution];
             foreach (var chunkPos in chunks)
             foreach (var chunkPos in chunks)
             {
             {
-                // TODO render on dedicated thread
-                Dispatcher.UIThread.Post(
-                    () =>
-                    {
-                        RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle,
-                            globalScaledClippingRectangle);
-                        RectI chunkRect = new(chunkPos * chunkSize, new(chunkSize, chunkSize));
-                        if (globalScaledClippingRectangle is RectI rect)
-                            chunkRect = chunkRect.Intersect(rect);
-
-                        infos.Add(new DirtyRect_RenderInfo(
-                            chunkRect.Pos,
-                            chunkRect.Size,
-                            resolution
-                        ));
-                    }, DispatcherPriority.Render);
+                RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle,
+                    globalScaledClippingRectangle);
+                RectI chunkRect = new(chunkPos * chunkSize, new(chunkSize, chunkSize));
+                if (globalScaledClippingRectangle is RectI rect)
+                    chunkRect = chunkRect.Intersect(rect);
+
+                /*infos.Add(new DirtyRect_RenderInfo(
+                    chunkRect.Pos,
+                    chunkRect.Size,
+                    resolution
+                ));*/
             }
             }
         }
         }
     }
     }
@@ -239,7 +268,7 @@ internal class CanvasUpdater
                     }
                     }
 
 
                     screenSurface.Surface.Canvas.DrawSurface(
                     screenSurface.Surface.Canvas.DrawSurface(
-                        chunk.Surface.Surface,
+                        chunk.Surface.DrawingSurface,
                         chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
                         chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
                     chunk.Dispose();
                     chunk.Dispose();
 
 
@@ -251,18 +280,18 @@ internal class CanvasUpdater
                 {
                 {
                     if (screenSurface.IsDisposed) return;
                     if (screenSurface.IsDisposed) return;
 
 
-                    if (globalScaledClippingRectangle is not null)
+                    /*if (globalScaledClippingRectangle is not null)
                     {
                     {
                         screenSurface.Surface.Canvas.Save();
                         screenSurface.Surface.Canvas.Save();
                         screenSurface.Surface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
                         screenSurface.Surface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
-                    }
+                    }*/
 
 
                     var pos = chunkPos * resolution.PixelSize();
                     var pos = chunkPos * resolution.PixelSize();
                     screenSurface.Surface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
                     screenSurface.Surface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
                         resolution.PixelSize(), ClearPaint);
                         resolution.PixelSize(), ClearPaint);
 
 
-                    if (globalScaledClippingRectangle is not null)
-                        screenSurface.Surface.Canvas.Restore();
+                    /*if (globalScaledClippingRectangle is not null)
+                        screenSurface.Surface.Canvas.Restore();*/
                 });
                 });
     }
     }
 }
 }

+ 6 - 6
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -560,7 +560,7 @@ internal class MemberPreviewUpdater
         AffectedArea area,
         AffectedArea area,
         VecI position, float scaling)
         VecI position, float scaling)
     {
     {
-        PostRender(() =>
+        QueueRender(() =>
         {
         {
             memberVM.PreviewSurface.Surface.Canvas.Save();
             memberVM.PreviewSurface.Surface.Canvas.Save();
             memberVM.PreviewSurface.Surface.Canvas.Scale(scaling);
             memberVM.PreviewSurface.Surface.Canvas.Scale(scaling);
@@ -608,7 +608,7 @@ internal class MemberPreviewUpdater
     private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Texture surface, AffectedArea area,
     private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Texture surface, AffectedArea area,
         VecI position, float scaling, int frame)
         VecI position, float scaling, int frame)
     {
     {
-        PostRender(() =>
+        QueueRender(() =>
         {
         {
             surface.Surface.Canvas.Save();
             surface.Surface.Canvas.Save();
             surface.Surface.Canvas.Scale(scaling);
             surface.Surface.Canvas.Scale(scaling);
@@ -643,7 +643,7 @@ internal class MemberPreviewUpdater
                 new Texture(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
                 new Texture(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
         }
         }
 
 
-        PostRender(() =>
+        QueueRender(() =>
         {
         {
             keyFrameVM.PreviewSurface!.Surface.Canvas.Save();
             keyFrameVM.PreviewSurface!.Surface.Canvas.Save();
             float scaling = (float)keyFrameVM.PreviewSurface.Size.X / internals.Tracker.Document.Size.X;
             float scaling = (float)keyFrameVM.PreviewSurface.Size.X / internals.Tracker.Document.Size.X;
@@ -706,7 +706,7 @@ internal class MemberPreviewUpdater
 
 
             var member = internals.Tracker.Document.FindMemberOrThrow(guid);
             var member = internals.Tracker.Document.FindMemberOrThrow(guid);
 
 
-            PostRender(() =>
+            QueueRender(() =>
             {
             {
                 memberVM.MaskPreviewSurface!.Surface.Canvas.Save();
                 memberVM.MaskPreviewSurface!.Surface.Canvas.Save();
                 memberVM.MaskPreviewSurface.Surface.Canvas.Scale(scaling);
                 memberVM.MaskPreviewSurface.Surface.Canvas.Scale(scaling);
@@ -754,7 +754,7 @@ internal class MemberPreviewUpdater
             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;
 
 
-            PostRender(() =>
+            QueueRender(() =>
             {
             {
                 nodeVm.ResultPreview.Surface.Canvas.Save();
                 nodeVm.ResultPreview.Surface.Canvas.Save();
                 nodeVm.ResultPreview.Surface.Canvas.Scale(scalingX, scalingY);
                 nodeVm.ResultPreview.Surface.Canvas.Scale(scalingX, scalingY);
@@ -771,7 +771,7 @@ internal class MemberPreviewUpdater
         }
         }
     }
     }
 
 
-    private void PostRender(Action action)
+    private void QueueRender(Action action)
     {
     {
         if (!DrawingBackendApi.Current.IsHardwareAccelerated)
         if (!DrawingBackendApi.Current.IsHardwareAccelerated)
         {
         {

+ 21 - 16
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">
@@ -48,24 +50,27 @@
                 </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>

+ 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
                 {
                 {

+ 17 - 3
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -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;

+ 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;
@@ -341,7 +343,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)
@@ -355,7 +357,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);
@@ -391,12 +393,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)

+ 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();
+    }
+}
+

+ 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>

+ 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);

+ 30 - 11
src/PixiEditor/Views/Rendering/Scene.cs

@@ -15,6 +15,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Skia;
 using PixiEditor.DrawingApi.Skia;
 using PixiEditor.DrawingApi.Skia.Extensions;
 using PixiEditor.DrawingApi.Skia.Extensions;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
@@ -107,6 +108,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     private List<Overlay> mouseOverOverlays = new();
     private List<Overlay> mouseOverOverlays = new();
 
 
     private double sceneOpacity = 1;
     private double sceneOpacity = 1;
+    
+    private static Scene instance;
 
 
     static Scene()
     static Scene()
     {
     {
@@ -136,9 +139,16 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         };
         };
     }
     }
 
 
+    public static event Action<Texture> Paint;
+
+    public static void RequestRender()
+    {
+        instance.QueueRender();
+    }
+
     public override void Render(DrawingContext context)
     public override void Render(DrawingContext context)
     {
     {
-        if (Surface == null || Surface.IsDisposed || Document == null) return;
+        //if (Surface == null || Document == null) return;
 
 
         float angle = (float)MathUtil.RadiansToDegrees(AngleRadians);
         float angle = (float)MathUtil.RadiansToDegrees(AngleRadians);
 
 
@@ -147,8 +157,8 @@ 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,
+        //Surface.Surface.Flush();
+        using var operation = new DrawSceneOperation(Paint, Document, CanvasPos, Scale * resolutionScale, angle,
             FlipX, FlipY,
             FlipX, FlipY,
             dirtyRect,
             dirtyRect,
             Bounds,
             Bounds,
@@ -490,15 +500,18 @@ internal class DrawSceneOperation : SkiaDrawOperation
 
 
     public RectI SurfaceRectToRender { get; }
     public RectI SurfaceRectToRender { get; }
 
 
+    public event Action<Texture> Paint;
+
     private SKPaint _paint = new SKPaint();
     private SKPaint _paint = new SKPaint();
 
 
     private bool hardwareAccelerationAvailable = DrawingBackendApi.Current.IsHardwareAccelerated;
     private bool hardwareAccelerationAvailable = DrawingBackendApi.Current.IsHardwareAccelerated;
 
 
-    public DrawSceneOperation(Texture surface, DocumentViewModel document, VecD contentPosition, double scale,
+    public DrawSceneOperation(Action<Texture> paint, 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)
     {
     {
-        Surface = surface;
+        //Surface = surface;
+        Paint += paint;
         Document = document;
         Document = document;
         ContentPosition = contentPosition;
         ContentPosition = contentPosition;
         Scale = scale;
         Scale = scale;
@@ -508,29 +521,35 @@ internal class DrawSceneOperation : SkiaDrawOperation
         ColorMatrix = colorMatrix;
         ColorMatrix = colorMatrix;
         ViewportBounds = viewportBounds;
         ViewportBounds = viewportBounds;
         _paint.Color = _paint.Color.WithAlpha((byte)(opacity * 255));
         _paint.Color = _paint.Color.WithAlpha((byte)(opacity * 255));
-        SurfaceRectToRender = FindRectToRender((float)scale);
+        //SurfaceRectToRender = FindRectToRender((float)scale);
+        SurfaceRectToRender = new RectI(VecI.Zero, Document.SizeBindable);
     }
     }
 
 
     public override void Render(ISkiaSharpApiLease lease)
     public override void Render(ISkiaSharpApiLease lease)
     {
     {
-        if (Surface == null || Surface.IsDisposed || Document == null) return;
+        //if (Surface == null || Surface.IsDisposed || Document == null) return;
 
 
         SKCanvas canvas = lease.SkCanvas;
         SKCanvas canvas = lease.SkCanvas;
 
 
         canvas.Save();
         canvas.Save();
 
 
-        if (SurfaceRectToRender.IsZeroOrNegativeArea)
+        /*if (SurfaceRectToRender.IsZeroOrNegativeArea)
         {
         {
             canvas.Restore();
             canvas.Restore();
             return;
             return;
-        }
+        }*/
 
 
         using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);
         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);
+        DrawingSurface drawingSurface = DrawingSurface.CreateFromNative(lease.SkSurface);
+        Texture texture = Texture.FromExisting(drawingSurface);
+        
+        Paint?.Invoke(texture);
+        
+        /*_paint.ColorFilter = SKColorFilter.CreateColorMatrix(matrixValues);
 
 
         if (!hardwareAccelerationAvailable)
         if (!hardwareAccelerationAvailable)
         {
         {
@@ -542,7 +561,7 @@ internal class DrawSceneOperation : SkiaDrawOperation
         else
         else
         {
         {
             canvas.DrawSurface(Surface.Surface.Native as SKSurface, 0, 0, _paint);
             canvas.DrawSurface(Surface.Surface.Native as SKSurface, 0, 0, _paint);
-        }
+        }*/
 
 
         canvas.Restore();
         canvas.Restore();
     }
     }

+ 10 - 11
src/PixiEditor/Views/Visuals/TextureControl.cs

@@ -21,9 +21,8 @@ public class TextureControl : Control
     public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<TextureControl, Stretch>(
     public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<TextureControl, Stretch>(
         nameof(Stretch), Stretch.Uniform);
         nameof(Stretch), Stretch.Uniform);
 
 
-    public static readonly StyledProperty<IBrush> BackgroundProperty =
-        AvaloniaProperty.Register<TextureControl, IBrush>(
-            nameof(Background));
+    public static readonly StyledProperty<IBrush> BackgroundProperty = AvaloniaProperty.Register<TextureControl, IBrush>
+    (nameof(Background));
 
 
     public Stretch Stretch
     public Stretch Stretch
     {
     {
@@ -99,14 +98,14 @@ public class TextureControl : Control
     {
     {
         if (Background != null)
         if (Background != null)
         {
         {
-            context.FillRectangle(Background, new Rect(0, 0, Bounds.Width, Bounds.Height));
+            context.FillRectangle(Background, new Rect(Bounds.Size));
         }
         }
-
-        if (Texture == null)
+        
+        if (Texture == null || Texture.IsDisposed)
         {
         {
             return;
             return;
         }
         }
-
+        
         Texture texture = Texture;
         Texture texture = Texture;
         texture.Surface.Flush();
         texture.Surface.Flush();
         ICustomDrawOperation drawOperation = new DrawTextureOperation(
         ICustomDrawOperation drawOperation = new DrawTextureOperation(
@@ -116,20 +115,20 @@ public class TextureControl : Control
 
 
         context.Custom(drawOperation);
         context.Custom(drawOperation);
     }
     }
-
+    
     private void OnTextureChanged(AvaloniaPropertyChangedEventArgs<Texture> args)
     private void OnTextureChanged(AvaloniaPropertyChangedEventArgs<Texture> args)
     {
     {
         if (args.OldValue.Value != null)
         if (args.OldValue.Value != null)
         {
         {
             args.OldValue.Value.Changed -= TextureOnChanged;
             args.OldValue.Value.Changed -= TextureOnChanged;
         }
         }
-
+        
         if (args.NewValue.Value != null)
         if (args.NewValue.Value != null)
         {
         {
             args.NewValue.Value.Changed += TextureOnChanged;
             args.NewValue.Value.Changed += TextureOnChanged;
         }
         }
     }
     }
-
+    
     private void TextureOnChanged(RectD? changedRect)
     private void TextureOnChanged(RectD? changedRect)
     {
     {
         Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
         Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
@@ -158,7 +157,7 @@ internal class DrawTextureOperation : SkiaDrawOperation
         {
         {
             return;
             return;
         }
         }
-
+        
         SKCanvas canvas = lease.SkCanvas;
         SKCanvas canvas = lease.SkCanvas;
 
 
         using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);
         using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);

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

@@ -20,6 +20,7 @@ public class TextureImage : IImage
 
 
     public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
     public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
     {
     {
+        Texture.Surface.Flush();
         context.Custom(new DrawTextureOperation(destRect, Stretch, Texture));
         context.Custom(new DrawTextureOperation(destRect, Stretch, Texture));
     }
     }
 }
 }