ソースを参照

Merge pull request #1155 from PixiEditor/async-rendering

Reworked whole backend -> UI rendering
Krzysztof Krysiński 3 週間 前
コミット
91fd773b13
95 ファイル変更2102 行追加1838 行削除
  1. 19 8
      src/ChunkyImageLib/Chunk.cs
  2. 96 4
      src/ChunkyImageLib/ChunkyImage.cs
  3. 88 9
      src/ChunkyImageLib/ChunkyImageEx.cs
  4. 1 1
      src/ChunkyImageLib/CommittedChunkStorage.cs
  5. 1 0
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  6. 1 1
      src/Drawie
  7. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Filter.cs
  8. 0 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyLayerNode.cs
  10. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNodeGraph.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  12. 12 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  13. 78 21
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  14. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CacheTriggerFlags.cs
  15. 11 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  16. 2 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  17. 30 35
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  18. 3 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs
  19. 0 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs
  20. 9 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  21. 20 21
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  22. 65 83
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  23. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  24. 15 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  25. 4 31
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  26. 25 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  27. 7 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  28. 12 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  29. 3 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  30. 19 24
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  31. 57 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  32. 4 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs
  33. 8 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  34. 59 75
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  35. 0 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  36. 25 17
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  37. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs
  38. 12 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  39. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  40. 3 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs
  41. 2 1
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  42. 0 17
      src/PixiEditor.ChangeableDocument/Helpers/PreviewUtils.cs
  43. 1 118
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  44. 43 0
      src/PixiEditor.ChangeableDocument/Rendering/PreviewRenderRequest.cs
  45. 48 0
      src/PixiEditor.ChangeableDocument/Rendering/PreviewUtility.cs
  46. 19 3
      src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs
  47. 27 0
      src/PixiEditor/Helpers/Converters/ResultPreviewIsPresentConverter.cs
  48. 35 25
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  49. 7 0
      src/PixiEditor/Models/DocumentPassthroughActions/RefreshPreview_PassthroughAction.cs
  50. 2 1
      src/PixiEditor/Models/Handlers/ICelHandler.cs
  51. 2 2
      src/PixiEditor/Models/Handlers/IDocument.cs
  52. 3 7
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  53. 3 2
      src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs
  54. 9 1
      src/PixiEditor/Models/Position/ViewportInfo.cs
  55. 40 2
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  56. 3 3
      src/PixiEditor/Models/Rendering/AnimationPreviewRenderer.cs
  57. 0 210
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  58. 272 103
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  59. 0 284
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  60. 240 148
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  61. 5 0
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  62. 7 0
      src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs
  63. 61 18
      src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs
  64. 15 4
      src/PixiEditor/Models/Serialization/Factories/SurfaceSerializationFactory.cs
  65. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  66. 3 3
      src/PixiEditor/Styles/Templates/KeyFrame.axaml
  67. 1 1
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  68. 18 8
      src/PixiEditor/Styles/Templates/NodeView.axaml
  69. 2 2
      src/PixiEditor/Styles/Templates/TimelineGroupHeader.axaml
  70. 8 8
      src/PixiEditor/ViewModels/Document/CelViewModel.cs
  71. 5 23
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  72. 15 24
      src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs
  73. 75 0
      src/PixiEditor/ViewModels/Document/TexturePreview.cs
  74. 6 8
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  75. 25 10
      src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs
  76. 2 0
      src/PixiEditor/ViewModels/ViewModelMain.cs
  77. 1 0
      src/PixiEditor/Views/Dock/DocumentPreviewDockView.axaml
  78. 4 6
      src/PixiEditor/Views/Layers/FolderControl.axaml
  79. 5 7
      src/PixiEditor/Views/Layers/LayerControl.axaml
  80. 1 0
      src/PixiEditor/Views/Main/DocumentPreview.axaml
  81. 8 0
      src/PixiEditor/Views/Main/DocumentPreview.axaml.cs
  82. 10 15
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml
  83. 58 14
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs
  84. 1 0
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  85. 50 9
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  86. 4 29
      src/PixiEditor/Views/Nodes/NodeView.cs
  87. 78 15
      src/PixiEditor/Views/Rendering/Scene.cs
  88. 0 227
      src/PixiEditor/Views/Visuals/PreviewPainterControl.cs
  89. 81 0
      src/PixiEditor/Views/Visuals/PreviewTextureControl.cs
  90. 1 1
      src/PixiParser
  91. 18 2
      tests/PixiEditor.Tests/BlendingTests.cs
  92. 49 4
      tests/PixiEditor.Tests/PixiEditorTest.cs
  93. 14 3
      tests/PixiEditor.Tests/RenderTests.cs
  94. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/BlendingLinearSrgb.png
  95. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleMasked.png

+ 19 - 8
src/ChunkyImageLib/Chunk.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
@@ -23,7 +24,7 @@ public class Chunk : IDisposable
     /// <summary>
     /// The surface of the chunk
     /// </summary>
-    public Surface Surface
+    public Texture Surface
     {
         get
         {
@@ -50,7 +51,7 @@ public class Chunk : IDisposable
 
     public bool Disposed => returned;
 
-    private Surface internalSurface;
+    private Texture internalSurface;
 
     private Chunk(ChunkResolution resolution, ColorSpace colorSpace)
     {
@@ -59,7 +60,7 @@ public class Chunk : IDisposable
         Resolution = resolution;
         ColorSpace = colorSpace;
         PixelSize = new(size, size);
-        internalSurface = new Surface(new ImageInfo(size, size, ColorType.RgbaF16, AlphaType.Premul, colorSpace));
+        internalSurface = new Texture(new ImageInfo(size, size, ColorType.RgbaF16, AlphaType.Premul, colorSpace) { GpuBacked = true });
     }
 
     /// <summary>
@@ -67,7 +68,10 @@ public class Chunk : IDisposable
     /// </summary>
     public static Chunk Create(ColorSpace chunkCs, ChunkResolution resolution = ChunkResolution.Full)
     {
-        var chunk = ChunkPool.Instance.Get(resolution, chunkCs);
+        return new Chunk(resolution, chunkCs);
+
+        // Leaving this in case chunk pooling turns out to be better
+        /*var chunk = ChunkPool.Instance.Get(resolution, chunkCs);
         if (chunk == null || chunk.Disposed)
         {
             chunk = new Chunk(resolution, chunkCs);
@@ -75,7 +79,7 @@ public class Chunk : IDisposable
 
         chunk.returned = false;
         Interlocked.Increment(ref chunkCounter);
-        return chunk;
+        return chunk;*/
     }
 
     /// <summary>
@@ -85,6 +89,7 @@ public class Chunk : IDisposable
     /// <param name="paint">The paint to use while drawing</param>
     public void DrawChunkOn(DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? samplingOptions = null)
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         if (samplingOptions == null || samplingOptions == SamplingOptions.Default)
         {
             surface.Canvas.DrawSurface(Surface.DrawingSurface, (float)pos.X, (float)pos.Y, paint);
@@ -98,6 +103,7 @@ public class Chunk : IDisposable
 
     public unsafe RectI? FindPreciseBounds(RectI? passedSearchRegion = null)
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         RectI? bounds = null;
         if (returned)
             return bounds;
@@ -109,7 +115,8 @@ public class Chunk : IDisposable
 
         RectI searchRegion = passedSearchRegion ?? new RectI(VecI.Zero, Surface.Size);
 
-        ulong* ptr = (ulong*)Surface.PixelBuffer;
+        using var pixmap = Surface.PeekPixels();
+        ulong* ptr = (ulong*)pixmap.GetPixels();
         for (int y = searchRegion.Top; y < searchRegion.Bottom; y++)
         {
             for (int x = searchRegion.Left; x < searchRegion.Right; x++)
@@ -133,11 +140,15 @@ public class Chunk : IDisposable
     /// </summary>
     public void Dispose()
     {
-        if (returned)
+        returned = true;
+        internalSurface.Dispose();
+        // Leaving this in case chunk pooling turns out to be better
+        /*if (returned)
             return;
         Interlocked.Decrement(ref chunkCounter);
         Surface.DrawingSurface.Canvas.Clear();
+        Surface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
         ChunkPool.Instance.Push(this);
-        returned = true;
+        returned = true;*/
     }
 }

+ 96 - 4
src/ChunkyImageLib/ChunkyImage.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
 using System.Runtime.CompilerServices;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
@@ -6,6 +7,7 @@ using OneOf;
 using OneOf.Types;
 using PixiEditor.Common;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
@@ -191,7 +193,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full, bool fallbackToChunkAligned = false)
+    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
+        bool fallbackToChunkAligned = false)
     {
         lock (lockObject)
         {
@@ -267,7 +270,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 var image = GetCommittedChunk(chunk, ChunkResolution.Full);
                 if (image is null)
                     continue;
-                output.EnqueueDrawImage(chunk * FullChunkSize, image.Surface);
+                output.EnqueueDrawTexture(chunk * FullChunkSize, image.Surface);
             }
 
             output.CommitChanges();
@@ -340,6 +343,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
 
             // something is queued, blend mode is not Src so we have to do merging
             {
+                using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
                 Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
                 Color committedColor = committedChunk is null
@@ -353,8 +357,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
                 using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
                 using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
-                tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, committedPaint);
-                tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, latestPaint);
+                tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecD(1)), committedPaint);
+                tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecI(1)), latestPaint);
                 return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
             }
         }
@@ -406,6 +410,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return false;
             }
 
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
             // combine with committed and then draw
             using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
             tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
@@ -421,6 +426,66 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
     }
 
+    public bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface,
+        VecD pos,
+        Paint? paint = null, SamplingOptions? sampling = null)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            OneOf<None, EmptyChunk, Chunk> latestChunk;
+            {
+                var chunk = MaybeGetLatestChunk(chunkPos, resolution);
+                if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
+                {
+                    latestChunk = new EmptyChunk();
+                }
+                else
+                {
+                    latestChunk = chunk is null ? new None() : chunk;
+                }
+            }
+
+            var committedChunk = GetCommittedChunk(chunkPos, resolution);
+
+            // draw committed directly
+            if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
+            {
+                if (committedChunk is null)
+                    return false;
+                committedChunk.DrawChunkOn(surface, pos, paint, sampling);
+                return true;
+            }
+
+            // no need to combine with committed, draw directly
+            if (blendMode == BlendMode.Src || committedChunk is null)
+            {
+                if (latestChunk.IsT2)
+                {
+                    latestChunk.AsT2.DrawChunkOn(surface, pos, paint, sampling);
+                    return true;
+                }
+
+                return false;
+            }
+
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
+            // combine with committed and then draw
+            using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
+            tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
+                ReplacingPaint);
+            blendModePaint.BlendMode = blendMode;
+            tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
+                blendModePaint);
+            if (lockTransparency)
+                OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
+            tempChunk.DrawChunkOn(surface, pos, paint, sampling);
+
+            return true;
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public bool LatestOrCommittedChunkExists(VecI chunkPos)
     {
@@ -955,6 +1020,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     {
         lock (lockObject)
         {
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
             ThrowIfDisposed();
             var affectedArea = FindAffectedArea();
 
@@ -1045,6 +1111,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                         continue;
                     }
 
+                    using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
                     //blend
                     blendModePaint.BlendMode = blendMode;
                     if (lockTransparency)
@@ -1124,6 +1192,26 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
     }
 
+    public Dictionary<VecI, Surface> CloneAllCommitedNonEmptyChunks()
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            var dict = new Dictionary<VecI, Surface>();
+            foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
+            {
+                if (chunk.FindPreciseBounds().HasValue)
+                {
+                    var surf = new Surface(chunk.Surface.ImageInfo);
+                    surf.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0);
+                    dict[pos] = surf;
+                }
+            }
+
+            return dict;
+        }
+    }
+
     /// <returns>
     /// Chunks affected by operations that haven't been committed yet
     /// </returns>
@@ -1259,6 +1347,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         if (operation is ClearOperation)
             return true;
 
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
         if (operation is IDrawOperation chunkOperation)
         {
             if (combinedRasterClips.IsT1) // Nothing is visible
@@ -1384,6 +1474,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// </summary>
     private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         // committed chunk of the same resolution exists
         Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
         if (targetChunk is not null)
@@ -1426,6 +1517,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// </summary>
     private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         // latest chunk exists
         Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
         if (targetChunk is not null)

+ 88 - 9
src/ChunkyImageLib/ChunkyImageEx.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib.DataHolders;
+using System.Diagnostics;
+using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
@@ -6,6 +7,7 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
 namespace ChunkyImageLib;
+
 public static class IReadOnlyChunkyImageEx
 {
     /// <summary>
@@ -20,11 +22,29 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawMostUpToDateRegionOn
     (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
-        VecD pos, Paint? paint = null, SamplingOptions? sampling = null)
+        VecD pos, Paint? paint = null, SamplingOptions? sampling = null, bool drawPaintOnEmpty = false)
+    {
+        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn, paint, sampling, drawPaintOnEmpty);
+    }
+
+    /// <summary>
+    /// Extracts a region from the <see cref="ChunkyImage"/> and draws it onto the passed <see cref="DrawingSurface"/>.
+    /// The region is taken from the most up to date version of the <see cref="ChunkyImage"/>
+    /// </summary>
+    /// <param name="image"><see cref="ChunkyImage"/> to extract the region from</param>
+    /// <param name="fullResRegion">The region to extract</param>
+    /// <param name="resolution">Chunk resolution</param>
+    /// <param name="surface">Surface to draw onto</param>
+    /// <param name="pos">Starting position on the surface</param>
+    /// <param name="paint">Paint to use for drawing</param>
+    public static void DrawMostUpToDateRegionOnWithAffected
+    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
+        AffectedArea affectedArea, VecD pos, Paint? paint = null, SamplingOptions? sampling = null, bool drawPaintOnEmpty = false)
     {
-        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn, paint, sampling);
+        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn,
+            image.DrawCachedMostUpToDateChunkOn, affectedArea, paint, sampling, drawPaintOnEmpty);
     }
-    
+
     /// <summary>
     /// Extracts a region from the <see cref="ChunkyImage"/> and draws it onto the passed <see cref="DrawingSurface"/>.
     /// The region is taken from the committed version of the <see cref="ChunkyImage"/>
@@ -36,18 +56,56 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawCommittedRegionOn
-        (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null, SamplingOptions? samplingOptions = null)
+    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
+        VecI pos, Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
+    {
+        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawCommittedChunkOn, paint, samplingOptions, drawPaintOnEmpty);
+    }
+
+    private static void DrawRegionOn(
+        RectI fullResRegion,
+        ChunkResolution resolution,
+        DrawingSurface surface,
+        VecD pos,
+        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
+        Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
     {
-        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawCommittedChunkOn, paint, samplingOptions);
+        int count = surface.Canvas.Save();
+        surface.Canvas.ClipRect(new RectD(pos, fullResRegion.Size));
+
+        VecI chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.TopLeft, ChunkyImage.FullChunkSize);
+        VecI chunkBotRight = OperationHelper.GetChunkPos(fullResRegion.BottomRight, ChunkyImage.FullChunkSize);
+        VecI offsetFullRes = (chunkTopLeft * ChunkyImage.FullChunkSize) - fullResRegion.Pos;
+        VecI offsetTargetRes = (VecI)(offsetFullRes * resolution.Multiplier());
+
+        for (int j = chunkTopLeft.Y; j <= chunkBotRight.Y; j++)
+        {
+            for (int i = chunkTopLeft.X; i <= chunkBotRight.X; i++)
+            {
+                var chunkPos = new VecI(i, j);
+                if (!drawingFunc(chunkPos, resolution, surface,
+                        offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
+                        samplingOptions) && paint != null && drawPaintOnEmpty)
+                {
+                    surface.Canvas.DrawRect(new RectD(
+                        offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
+                        new VecD(resolution.PixelSize())), paint);
+                }
+            }
+        }
+
+        surface.Canvas.RestoreToCount(count);
     }
-    
+
     private static void DrawRegionOn(
         RectI fullResRegion,
         ChunkResolution resolution,
         DrawingSurface surface,
         VecD pos,
         Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
-        Paint? paint = null, SamplingOptions? samplingOptions = null)
+        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> quickDrawingFunc,
+        AffectedArea area,
+        Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
     {
         int count = surface.Canvas.Save();
         surface.Canvas.ClipRect(new RectD(pos, fullResRegion.Size));
@@ -62,7 +120,28 @@ public static class IReadOnlyChunkyImageEx
             for (int i = chunkTopLeft.X; i <= chunkBotRight.X; i++)
             {
                 var chunkPos = new VecI(i, j);
-                drawingFunc(chunkPos, resolution, surface, offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint, samplingOptions);
+                if (area.Chunks != null && area.Chunks.Contains(chunkPos))
+                {
+                    if (!drawingFunc(chunkPos, resolution, surface,
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
+                            samplingOptions) && paint != null && drawPaintOnEmpty)
+                    {
+                        surface.Canvas.DrawRect(new RectD(
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
+                            new VecD(resolution.PixelSize())), paint);
+                    }
+                }
+                else
+                {
+                    if (!quickDrawingFunc(chunkPos, resolution, surface,
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
+                            samplingOptions) && paint != null && drawPaintOnEmpty)
+                    {
+                        surface.Canvas.DrawRect(new RectD(
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
+                            new VecD(resolution.PixelSize())), paint);
+                    }
+                }
             }
         }
 

+ 1 - 1
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -36,7 +36,7 @@ public class CommittedChunkStorage : IDisposable
             if (chunk is null)
                 image.EnqueueClearRegion(new(pos * ChunkPool.FullChunkSize, new(ChunkPool.FullChunkSize, ChunkPool.FullChunkSize)));
             else
-                image.EnqueueDrawImage(pos * ChunkPool.FullChunkSize, chunk.Surface, ReplacingPaint);
+                image.EnqueueDrawTexture(pos * ChunkPool.FullChunkSize, chunk.Surface, ReplacingPaint);
         }
     }
 

+ 1 - 0
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -11,6 +11,7 @@ namespace ChunkyImageLib;
 public interface IReadOnlyChunkyImage
 {
     bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
+    bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
     bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
     RectI? FindChunkAlignedMostUpToDateBounds();
     RectI? FindChunkAlignedCommittedBounds();

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit b3b3a342c4b9d188de984ecefd4a9f8d020d6d4c
+Subproject commit 753784f70c3a455dce172ad2e92be63329f9d4c3

+ 10 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Filter.cs

@@ -1,9 +1,10 @@
 using System.Diagnostics.Contracts;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using PixiEditor.Common;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
-public sealed class Filter : IDisposable
+public sealed class Filter : IDisposable, ICacheable
 {
     public Filter(ColorFilter? colorFilter, ImageFilter? imageFilter)
     {
@@ -45,4 +46,12 @@ public sealed class Filter : IDisposable
         ColorFilter?.Dispose();
         ImageFilter?.Dispose();
     }
+
+    public int GetCacheHash()
+    {
+        HashCode hash = new();
+        hash.Add(ColorFilter?.GetHashCode() ?? 0);
+        hash.Add(ImageFilter?.GetHashCode() ?? 0);
+        return hash.ToHashCode();
+    }
 }

+ 0 - 13
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs

@@ -1,13 +0,0 @@
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces;
-using Drawie.Numerics;
-using PixiEditor.ChangeableDocument.Rendering;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-
-public interface IPreviewRenderable
-{
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = ""); 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context,
-        string elementToRenderName);
-}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyLayerNode.cs

@@ -1,5 +1,5 @@
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
-public interface IReadOnlyLayerNode : IReadOnlyStructureNode, IPreviewRenderable
+public interface IReadOnlyLayerNode : IReadOnlyStructureNode
 {
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNodeGraph.cs

@@ -4,7 +4,7 @@ using PixiEditor.Common;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
-public interface IReadOnlyNodeGraph : ICacheable
+public interface IReadOnlyNodeGraph : ICacheable, IDisposable
 {
     public IReadOnlyCollection<IReadOnlyNode> AllNodes { get; }
     public IReadOnlyNode OutputNode { get; }
@@ -13,4 +13,5 @@ public interface IReadOnlyNodeGraph : ICacheable
     public bool TryTraverse(Action<IReadOnlyNode> action);
     public void Execute(RenderContext context);
     Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode endNode);
+    public IReadOnlyNodeGraph Clone();
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs

@@ -8,7 +8,7 @@ using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
-public interface IReadOnlyStructureNode : IReadOnlyNode, ISceneObject, IChunkRenderable
+public interface IReadOnlyStructureNode : IReadOnlyNode, ISceneObject
 {
     public InputProperty<float> Opacity { get; }
     public InputProperty<bool> IsVisible { get; }

+ 12 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs

@@ -19,4 +19,16 @@ public class SceneObjectRenderContext : RenderContext
         LocalBounds = localBounds;
         RenderSurfaceIsScene = renderSurfaceIsScene;
     }
+
+    public override RenderContext Clone()
+    {
+        return new SceneObjectRenderContext(TargetPropertyOutput, RenderSurface, LocalBounds, FrameTime, ChunkResolution, RenderOutputSize, DocumentSize, RenderSurfaceIsScene, ProcessingColorSpace, DesiredSamplingOptions, Opacity)
+        {
+            VisibleDocumentRegion = VisibleDocumentRegion,
+            AffectedArea = AffectedArea,
+            FullRerender = FullRerender,
+            TargetOutput = TargetOutput,
+            PreviewTextures = PreviewTextures,
+        };
+    }
 }

+ 78 - 21
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -1,14 +1,15 @@
 using System.Collections.Immutable;
+using System.Diagnostics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
-public class NodeGraph : IReadOnlyNodeGraph, IDisposable
+public class NodeGraph : IReadOnlyNodeGraph
 {
     private ImmutableList<IReadOnlyNode>? cachedExecutionList;
-    
+
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public IReadOnlyDictionary<Guid, Node> NodeLookup => nodeLookup;
@@ -27,7 +28,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         {
             return;
         }
-        
+
         node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
         nodeLookup[node.Id] = node;
@@ -61,7 +62,53 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
         return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
     }
-    
+
+    public IReadOnlyNodeGraph Clone()
+    {
+        var newGraph = new NodeGraph();
+        var nodeMapping = new Dictionary<Node, Node>();
+
+        // Clone nodes
+        foreach (var node in Nodes)
+        {
+            var clonedNode = node.Clone(true);
+            newGraph.AddNode(clonedNode);
+            nodeMapping[node] = clonedNode;
+        }
+
+        // Re-establish connections
+        foreach (var node in Nodes)
+        {
+            var clonedNode = nodeMapping[node];
+            foreach (var input in node.InputProperties)
+            {
+                if (input.Connection != null)
+                {
+                    var connectedNode = input.Connection.Node;
+                    if (nodeMapping.TryGetValue(connectedNode as Node, out var clonedConnectedNode))
+                    {
+                        var clonedOutput = clonedConnectedNode.OutputProperties.FirstOrDefault(o =>
+                            o.InternalPropertyName == input.Connection.InternalPropertyName);
+                        var clonedInput = clonedNode.InputProperties.FirstOrDefault(i =>
+                            i.InternalPropertyName == input.InternalPropertyName);
+                        if (clonedOutput != null && clonedInput != null)
+                        {
+                            clonedOutput.ConnectTo(clonedInput);
+                        }
+                    }
+                }
+            }
+        }
+
+        // Set custom output node if applicable
+        if (CustomOutputNode != null && nodeMapping.TryGetValue(CustomOutputNode, out var mappedOutputNode))
+        {
+            newGraph.CustomOutputNode = mappedOutputNode;
+        }
+
+        return newGraph;
+    }
+
     private ImmutableList<IReadOnlyNode> CalculateExecutionQueueInternal(IReadOnlyNode outputNode)
     {
         return cachedExecutionList ??= GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
@@ -81,40 +128,49 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
     public bool TryTraverse(Action<IReadOnlyNode> action)
     {
-        if(OutputNode == null) return false;
-        
+        if (OutputNode == null) return false;
+
         var queue = CalculateExecutionQueueInternal(OutputNode);
-        
+
         foreach (var node in queue)
         {
             action(node);
         }
-        
+
         return true;
     }
 
+    bool isexecuting = false;
+
     public void Execute(RenderContext context)
     {
+        if (isexecuting) return;
+        isexecuting = true;
         if (OutputNode == null) return;
-        if(!CanExecute()) return;
+        if (!CanExecute()) return;
 
         var queue = CalculateExecutionQueueInternal(OutputNode);
-        
+
         foreach (var node in queue)
         {
-            if (node is Node typedNode)
-            {
-                if(typedNode.IsDisposed) continue;
-                
-                typedNode.ExecuteInternal(context);
-            }
-            else
+            lock (node)
             {
-                node.Execute(context);
+                if (node is Node typedNode)
+                {
+                    if (typedNode.IsDisposed) continue;
+
+                    typedNode.ExecuteInternal(context);
+                }
+                else
+                {
+                    node.Execute(context);
+                }
             }
         }
+
+        isexecuting = false;
     }
-    
+
     private bool CanExecute()
     {
         foreach (var node in Nodes)
@@ -127,7 +183,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
         return true;
     }
-    
+
     private void ResetCache()
     {
         cachedExecutionList = null;
@@ -138,7 +194,8 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         HashCode hash = new();
         foreach (var node in Nodes)
         {
-            hash.Add(node.GetCacheHash());
+            int nodeCache = node.GetCacheHash();
+            hash.Add(nodeCache);
         }
 
         return hash.ToHashCode();

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CacheTriggerFlags.cs

@@ -6,5 +6,6 @@ public enum CacheTriggerFlags
     None = 0,
     Inputs = 1,
     Timeline = 2,
-    All = Inputs | Timeline
+    RenderSize = 4,
+    All = Inputs | Timeline | RenderSize
 }

+ 11 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs

@@ -82,9 +82,10 @@ public class CombineChannelsNode : RenderNode
         surface.Canvas.RestoreToCount(saved);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
-        RectD? redBounds = PreviewUtils.FindPreviewBounds(Red.Connection, frame, elementToRenderName);
+        int frame = ctx.FrameTime.Frame;
+        /*RectD? redBounds = PreviewUtils.FindPreviewBounds(Red.Connection, frame, elementToRenderName);
         RectD? greenBounds = PreviewUtils.FindPreviewBounds(Green.Connection, frame, elementToRenderName);
         RectD? blueBounds = PreviewUtils.FindPreviewBounds(Blue.Connection, frame, elementToRenderName);
         RectD? alphaBounds = PreviewUtils.FindPreviewBounds(Alpha.Connection, frame, elementToRenderName);
@@ -116,19 +117,18 @@ public class CombineChannelsNode : RenderNode
             finalBounds = finalBounds?.Union(alphaBounds.Value) ?? alphaBounds.Value;
         }
         
-        return finalBounds;
+        return finalBounds;*/
+        return null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
-        if (Red.Value == null && Green.Value == null && Blue.Value == null && Alpha.Value == null)
-        {
-            return false;
-        }
+        return Red.Value != null || Green.Value != null || Blue.Value != null || Alpha.Value != null;
+    }
 
-        OnPaint(context, renderOn); 
-        
-        return true;
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        OnPaint(context, renderOn);
     }
 
     public override Node CreateCopy() => new CombineChannelsNode();

+ 2 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -10,7 +10,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
 [NodeInfo("SeparateChannels")]
-public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
+public class SeparateChannelsNode : Node, IRenderInput
 {
     private readonly Paint _paint = new();
     
@@ -96,8 +96,7 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
     RenderInputProperty IRenderInput.Background => Image;
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
-        RectD? bounds = PreviewUtils.FindPreviewBounds(Image.Connection, frame, elementToRenderName);
-        return bounds;
+        return null;
     }
 
     public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)

+ 30 - 35
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -13,7 +13,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("CreateImage")]
-public class CreateImageNode : Node, IPreviewRenderable
+public class CreateImageNode : Node
 {
     public OutputProperty<Texture> Output { get; }
 
@@ -30,6 +30,9 @@ public class CreateImageNode : Node, IPreviewRenderable
 
     private TextureCache textureCache = new();
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
@@ -48,6 +51,7 @@ public class CreateImageNode : Node, IPreviewRenderable
         }
 
         var surface = Render(context);
+        RenderPreviews(surface, context);
 
         Output.Value = surface;
 
@@ -56,7 +60,8 @@ public class CreateImageNode : Node, IPreviewRenderable
 
     private Texture Render(RenderContext context)
     {
-        var surface = textureCache.RequestTexture(0, (VecI)(Size.Value * context.ChunkResolution.Multiplier()), context.ProcessingColorSpace, false);
+        int id = (Size.Value * context.ChunkResolution.Multiplier()).GetHashCode();
+        var surface = textureCache.RequestTexture(id, (VecI)(Size.Value * context.ChunkResolution.Multiplier()), context.ProcessingColorSpace, false);
         surface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
 
         if (Fill.Value is ColorPaintable colorPaintable)
@@ -99,45 +104,35 @@ public class CreateImageNode : Node, IPreviewRenderable
         surface.Canvas.RestoreToCount(saved);
     }
 
-    public override Node CreateCopy() => new CreateImageNode();
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        textureCache.Dispose();
-    }
-
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    private void RenderPreviews(Texture surface, RenderContext context)
     {
-        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        var previews = context.GetPreviewTexturesForNode(Id);
+        if (previews is null) return;
+        foreach (var request in previews)
         {
-            return null;
-        }
+            var texture = request.Texture;
+            if (texture is null) continue;
 
-        return new RectD(0, 0, Size.Value.X, Size.Value.Y);
-    }
+            int saved = texture.DrawingSurface.Canvas.Save();
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
-        {
-            return false;
-        }
+            VecD scaling = PreviewUtility.CalculateUniformScaling(surface.Size, texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(surface.Size, texture.Size, scaling);
+            texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            var previewCtx =
+                PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
 
-        if (Output.Value == null)
-        {
-            return false;
+            texture.DrawingSurface.Canvas.Clear();
+            texture.DrawingSurface.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
         }
+    }
 
-        var surface = Render(context);
-        
-        if (surface == null || surface.IsDisposed)
-        {
-            return false;
-        }
-        
-        renderOn.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
-        
-        return true;
+    public override Node CreateCopy() => new CreateImageNode();
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose();
     }
 }

+ 3 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs

@@ -32,7 +32,6 @@ public class OutlineNode : RenderNode, IRenderInput
     private ImageFilter filter;
 
     private OutlineType? lastType = null;
-    private VecI lastDocumentSize;
 
     protected override bool ExecuteOnlyOnCacheChange => true;
 
@@ -52,7 +51,6 @@ public class OutlineNode : RenderNode, IRenderInput
     protected override void OnExecute(RenderContext context)
     {
         base.OnExecute(context);
-        lastDocumentSize = context.RenderOutputSize;
 
         Kernel finalKernel = Type.Value switch
         {
@@ -118,18 +116,17 @@ public class OutlineNode : RenderNode, IRenderInput
         Background?.Value?.Paint(context, surface);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+        return new RectD(0, 0, ctx.DocumentSize.X, ctx.DocumentSize.Y);
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         int saved = renderOn.Canvas.Save();
         renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
         OnPaint(context, renderOn);
         renderOn.Canvas.RestoreToCount(saved);
-        return true;
     }
 
     public override Node CreateCopy()

+ 0 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs

@@ -141,17 +141,6 @@ public class PosterizationNode : RenderNode, IRenderInput
         surface.Canvas.DrawSurface(temp.DrawingSurface, 0, 0);
         surface.Canvas.RestoreToCount(saved);
     }
-    
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        OnPaint(context, renderOn);
-        return true;
-    }
 
     public override Node CreateCopy()
     {

+ 9 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs

@@ -28,6 +28,9 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
     
     public InputProperty<bool> InvertMask { get; }
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
     public ApplyFilterNode()
     {
         Background = CreateRenderInput("Input", "IMAGE");
@@ -40,7 +43,7 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
 
     protected override void Paint(RenderContext context, DrawingSurface surface)
     {
-        AllowHighDpiRendering = (Background.Connection.Node as RenderNode)?.AllowHighDpiRendering ?? true;
+        AllowHighDpiRendering = (Background.Connection?.Node as RenderNode)?.AllowHighDpiRendering ?? true;
         base.Paint(context, surface);
     }
 
@@ -119,8 +122,11 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         finalSurface.Canvas.RestoreToCount(saved);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "") =>
-        PreviewUtils.FindPreviewBounds(Background.Connection, frame, elementToRenderName);
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "") =>
+        null;
+        /*
+        PreviewUtils.FindPreviewBounds(Background.Connection, ctx.FrameTime.Frame, elementToRenderName);
+        */
 
     public override Node CreateCopy() => new ApplyFilterNode();
 

+ 20 - 21
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -44,6 +44,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
 
     public override void Render(SceneObjectRenderContext sceneContext)
     {
+        RenderPreviews(sceneContext);
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         {
             Output.Value = Background.Value;
@@ -99,8 +100,13 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
 
         Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface);
 
+        int saved2 = outputWorkingSurface.DrawingSurface.Canvas.Save();
+        outputWorkingSurface.DrawingSurface.Canvas.Scale((float)sceneContext.ChunkResolution.InvertedMultiplier());
+
         ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext, sceneContext.ChunkResolution);
 
+        outputWorkingSurface.DrawingSurface.Canvas.RestoreToCount(saved2);
+
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         {
             Texture tempSurface = RequestTexture(1, outputWorkingSurface.Size, sceneContext.ProcessingColorSpace);
@@ -238,44 +244,37 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         return guids;
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
-        if (elementFor == nameof(EmbeddedMask))
+        if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return base.GetPreviewBounds(frame, elementFor);
+            return base.ShouldRenderPreview(elementToRenderName);
         }
 
-        return GetApproxBounds(frame);
+        return Content.Connection != null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
+    {
+        return GetApproxBounds(ctx.FrameTime);
+    }
+
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return base.RenderPreview(renderOn, context, elementToRenderName);
+            base.RenderPreview(renderOn, context, elementToRenderName);
+            return;
         }
 
         if (Content.Connection != null)
         {
-            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node, FilterInvisibleFolders);
-            while (executionQueue.Count > 0)
+            if (context is SceneObjectRenderContext ctx)
             {
-                IReadOnlyNode node = executionQueue.Dequeue();
-
-                if (node is IReadOnlyStructureNode { IsVisible.Value: false })
-                {
-                    continue;
-                }
-
-                if (node is IPreviewRenderable previewRenderable)
-                {
-                    previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
-                }
+                RenderFolderContent(ctx, true);
             }
         }
-
-        return true;
     }
 
     void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)

+ 65 - 83
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Helpers;
@@ -28,10 +29,9 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     private VecI startSize;
     private ColorSpace colorSpace;
-    private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
-    private Texture fullResrenderedSurface;
-    private int renderedSurfaceFrame = -1;
+
+    private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
     public ImageLayerNode(VecI size, ColorSpace colorSpace)
     {
@@ -116,49 +116,53 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
         Paint paint)
     {
-        DrawLayer(workingSurface, paint, ctx);
+        DrawLayer(workingSurface, paint, ctx, false);
     }
 
     protected override void DrawWithFilters(SceneObjectRenderContext context, DrawingSurface workingSurface,
         Paint paint)
     {
-        DrawLayer(workingSurface, paint, context);
+        DrawLayer(workingSurface, paint, context, true);
     }
 
-    private void DrawLayer(DrawingSurface workingSurface, Paint paint, SceneObjectRenderContext ctx)
+    private void DrawLayer(DrawingSurface workingSurface, Paint paint, SceneObjectRenderContext ctx, bool saveLayer)
     {
         int saved = workingSurface.Canvas.Save();
 
         var sceneSize = GetSceneSize(ctx.FrameTime);
-        VecD topLeft = sceneSize / 2f;
+        RectI latestSize = new(0, 0, layerImage.LatestSize.X, layerImage.LatestSize.Y);
+        var region = ctx.VisibleDocumentRegion ?? latestSize;
+
+        VecD topLeft = region.TopLeft - sceneSize / 2;
 
-        if (renderedSurfaceFrame == null || ctx.FullRerender || ctx.FrameTime.Frame != renderedSurfaceFrame)
+        topLeft *= ctx.ChunkResolution.Multiplier();
+        workingSurface.Canvas.Scale((float)ctx.ChunkResolution.InvertedMultiplier());
+        var img = GetLayerImageAtFrame(ctx.FrameTime.Frame);
+
+        if (saveLayer)
         {
-            GetLayerImageAtFrame(ctx.FrameTime.Frame).DrawMostUpToDateRegionOn(
-                new RectI(0, 0, layerImage.LatestSize.X, layerImage.LatestSize.Y),
-                ChunkResolution.Full,
-                workingSurface, -topLeft, paint);
+            workingSurface.Canvas.SaveLayer(paint);
+        }
+
+        if (!ctx.FullRerender)
+        {
+            img.DrawMostUpToDateRegionOnWithAffected(
+                region,
+                ctx.ChunkResolution,
+                workingSurface, ctx.AffectedArea, topLeft, saveLayer ? null : paint, ctx.DesiredSamplingOptions);
         }
         else
         {
-            if (ctx.DesiredSamplingOptions == SamplingOptions.Default)
-            {
-                workingSurface.Canvas.DrawSurface(
-                    fullResrenderedSurface.DrawingSurface, -(float)topLeft.X, -(float)topLeft.Y, paint);
-            }
-            else
-            {
-                using var snapshot = fullResrenderedSurface.DrawingSurface.Snapshot();
-                workingSurface.Canvas.DrawImage(snapshot, -(float)topLeft.X, -(float)topLeft.Y,
-                    ctx.DesiredSamplingOptions,
-                    paint);
-            }
+            img.DrawMostUpToDateRegionOn(
+                region,
+                ctx.ChunkResolution,
+                workingSurface, topLeft, saveLayer ? null : paint, ctx.DesiredSamplingOptions);
         }
 
         workingSurface.Canvas.RestoreToCount(saved);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    public override RectD? GetPreviewBounds(RenderContext context, string elementFor = "")
     {
         if (IsDisposed)
         {
@@ -167,11 +171,16 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
         if (elementFor == nameof(EmbeddedMask))
         {
-            return base.GetPreviewBounds(frame, elementFor);
+            return base.GetPreviewBounds(context, elementFor);
         }
 
         if (Guid.TryParse(elementFor, out Guid guid))
         {
+            if (guid == Id)
+            {
+                return new RectD(0, 0, layerImage.CommittedSize.X, layerImage.CommittedSize.Y);
+            }
+
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
 
             if (keyFrame != null)
@@ -182,19 +191,13 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                     return null;
                 }
 
-                RectI? bounds = (RectI?)GetApproxBounds(kf);
-                if (bounds.HasValue)
-                {
-                    return new RectD(bounds.Value.X, bounds.Value.Y,
-                        Math.Min(bounds.Value.Width, kf.CommittedSize.X),
-                        Math.Min(bounds.Value.Height, kf.CommittedSize.Y));
-                }
+                return new RectD(0, 0, kf.CommittedSize.X, kf.CommittedSize.Y);
             }
         }
 
         try
         {
-            var kf = GetLayerImageAtFrame(frame);
+            var kf = GetLayerImageAtFrame(context.FrameTime.Frame);
             if (kf == null)
             {
                 return null;
@@ -216,8 +219,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         }
     }
 
-    public override bool RenderPreview(DrawingSurface renderOnto, RenderContext context,
-        string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
         if (IsDisposed)
         {
@@ -226,12 +228,28 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
         if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return base.RenderPreview(renderOnto, context, elementToRenderName);
+            return base.ShouldRenderPreview(elementToRenderName);
+        }
+
+        return true;
+    }
+
+    public override void RenderPreview(DrawingSurface renderOnto, RenderContext context,
+        string elementToRenderName)
+    {
+        if (IsDisposed)
+        {
+            return;
+        }
+
+        if (elementToRenderName == nameof(EmbeddedMask))
+        {
+            base.RenderPreview(renderOnto, context, elementToRenderName);
+            return;
         }
 
         var img = GetLayerImageAtFrame(context.FrameTime.Frame);
 
-        int cacheFrame = context.FrameTime.Frame;
         if (Guid.TryParse(elementToRenderName, out Guid guid))
         {
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
@@ -239,46 +257,27 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             if (keyFrame != null)
             {
                 img = GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid);
-                cacheFrame = keyFrame.StartFrame;
             }
             else if (guid == Id)
             {
                 img = GetLayerImageAtFrame(0);
-                cacheFrame = 0;
             }
         }
 
         if (img is null)
         {
-            return false;
+            return;
         }
 
-        if (renderedSurfaceFrame == cacheFrame)
-        {
-            int saved = renderOnto.Canvas.Save();
-            renderOnto.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-            if (context.DesiredSamplingOptions == SamplingOptions.Default)
-            {
-                renderOnto.Canvas.DrawSurface(
-                    fullResrenderedSurface.DrawingSurface, 0, 0, blendPaint);
-            }
-            else
-            {
-                using var snapshot = fullResrenderedSurface.DrawingSurface.Snapshot();
-                renderOnto.Canvas.DrawImage(snapshot, 0, 0, context.DesiredSamplingOptions, blendPaint);
-            }
+        int saved = renderOnto.Canvas.Save();
+        renderOnto.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
 
-            renderOnto.Canvas.RestoreToCount(saved);
-        }
-        else
-        {
-            img.DrawMostUpToDateRegionOn(
-                new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
-                context.ChunkResolution,
-                renderOnto, VecI.Zero, blendPaint, context.DesiredSamplingOptions);
-        }
+        img.DrawCommittedRegionOn(
+            new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
+            context.ChunkResolution,
+            renderOnto, VecI.Zero, replacePaint, context.DesiredSamplingOptions);
 
-        return true;
+        renderOnto.Canvas.RestoreToCount(saved);
     }
 
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
@@ -330,12 +329,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return image;
     }
 
-    public override void Dispose()
-    {
-        base.Dispose();
-        fullResrenderedSurface?.Dispose();
-    }
-
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
 
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageByKeyFrameGuid(Guid keyFrameGuid) =>
@@ -346,17 +339,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
 
-    public override void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
-        ColorSpace processColorSpace)
-    {
-        base.RenderChunk(chunkPos, resolution, frameTime, processColorSpace);
-
-        var img = GetLayerImageAtFrame(frameTime.Frame);
-
-        RenderChunkyImageChunk(chunkPos, resolution, img, 85, processColorSpace, ref fullResrenderedSurface);
-        renderedSurfaceFrame = frameTime.Frame;
-    }
-
     public void ForEveryFrame(Action<ChunkyImage> action)
     {
         foreach (var frame in keyFrames)

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -22,6 +22,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
     public override void Render(SceneObjectRenderContext sceneContext)
     {
+        RenderPreviews(sceneContext);
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         {
             Output.Value = Background.Value;
@@ -44,6 +45,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             }
 
+            // TODO: Optimizattion: Simple graphs can draw directly to scene, skipping the intermediate surface
             if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.RenderOutputSize)
             {
                 DrawLayerInScene(context, renderOnto, useFilters);
@@ -59,7 +61,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 var tempSurface = TryInitWorkingSurface(context.RenderOutputSize, context.ChunkResolution,
                     context.ProcessingColorSpace, 22);
 
-                DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters, targetPaint);
+                DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters,
+                    targetPaint);
 
                 blendPaint.SetFilters(null);
                 DrawWithResolution(tempSurface.DrawingSurface, renderOnto, context.ChunkResolution,
@@ -151,7 +154,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         workingSurface.Canvas.RestoreToCount(scaled);
     }
 
-    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution, SamplingOptions sampling)
+    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution,
+        SamplingOptions sampling)
     {
         int scaled = target.Canvas.Save();
         float multiplier = (float)resolution.InvertedMultiplier();

+ 15 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs

@@ -40,30 +40,36 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
 
         Float3x3 mtx = Matrix.Value.Invoke(FuncContext.NoContext);
 
+        Matrix3X3 constant = mtx.GetConstant() as Matrix3X3? ?? Matrix3X3.Identity;
         surface.Canvas.SetMatrix(
-            surface.Canvas.TotalMatrix.Concat(mtx.GetConstant() as Matrix3X3? ?? Matrix3X3.Identity));
+            surface.Canvas.TotalMatrix.Concat(constant));
+
+        var clonedCtx = context.Clone();
+        if (clonedCtx.VisibleDocumentRegion.HasValue)
+        {
+            clonedCtx.VisibleDocumentRegion =
+                (RectI)constant.Invert().TransformRect((RectD)clonedCtx.VisibleDocumentRegion.Value);
+        }
+
         if (!surface.LocalClipBounds.IsZeroOrNegativeArea)
         {
-            Background.Value?.Paint(context, surface);
+            Background.Value?.Paint(clonedCtx, surface);
         }
 
         surface.Canvas.RestoreToCount(layer);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
         if (Background.Value == null)
             return null;
 
-        return base.GetPreviewBounds(frame, elementToRenderName);
+        return base.GetPreviewBounds(ctx, elementToRenderName);
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
-        if (Background.Value == null)
-            return false;
-
-        return base.RenderPreview(renderOn, context, elementToRenderName);
+        return Background.Value != null;
     }
 
     protected abstract Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input);

+ 4 - 31
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -70,46 +70,19 @@ public class MergeNode : RenderNode
         Top.Value?.Paint(context, target);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
-        if (Top.Value == null && Bottom.Value == null)
-        {
-            return null;
-        }
-
-        RectD? totalBounds = null;
-
-        if (Top.Connection != null && Top.Connection.Node is IPreviewRenderable topPreview)
-        {
-            var topBounds = topPreview.GetPreviewBounds(frame, elementToRenderName);
-            if (topBounds != null)
-            {
-                totalBounds = totalBounds?.Union(topBounds.Value) ?? topBounds;
-            }
-        }
-
-        if (Bottom.Connection != null && Bottom.Connection.Node is IPreviewRenderable bottomPreview)
-        {
-            var bottomBounds = bottomPreview.GetPreviewBounds(frame, elementToRenderName);
-            if (bottomBounds != null)
-            {
-                totalBounds = totalBounds?.Union(bottomBounds.Value) ?? bottomBounds;
-            }
-        }
-
-        return totalBounds;
+        return Top.Value != null || Bottom.Value != null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if (Top.Value == null && Bottom.Value == null)
         {
-            return false;
+            return;
         }
 
         Merge(renderOn, context);
-
-        return true;
     }
 
     public override void Dispose()

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

@@ -12,7 +12,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("ModifyImageLeft")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
-public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
+public class ModifyImageLeftNode : Node, IPairNode
 {
     public InputProperty<Texture?> Image { get; }
 
@@ -48,20 +48,37 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
 
     protected override void OnExecute(RenderContext context)
     {
+        RenderPreviews(context);
     }
 
     public override Node CreateCopy() => new ModifyImageLeftNode();
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+
+    private void RenderPreviews(RenderContext context)
     {
-        if(Image.Value == null)
+        var previews = context.GetPreviewTexturesForNode(Id);
+        if (previews is null) return;
+        foreach (var request in previews)
         {
-            return null;
-        } 
-        
-        return new RectD(0, 0, Image.Value.Size.X, Image.Value.Size.Y);
+            var texture = request.Texture;
+            if (texture is null) continue;
+
+            int saved = texture.DrawingSurface.Canvas.Save();
+
+            VecI size = Image.Value?.Size ?? context.RenderOutputSize;
+            VecD scaling = PreviewUtility.CalculateUniformScaling(size, texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(size, texture.Size, scaling);
+            texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            var previewCtx =
+                PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
+
+            texture.DrawingSurface.Canvas.Clear();
+            RenderPreview(texture.DrawingSurface);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
     }
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public bool RenderPreview(DrawingSurface renderOn)
     {
         if(Image.Value is null)
         {

+ 7 - 13
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
@@ -114,28 +115,21 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         builder.Dispose();
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
     {
-        var startNode = FindStartNode();
-        if (startNode != null)
-        {
-            return startNode.GetPreviewBounds(frame, elementToRenderName);
-        }
-
-        return null;
+        return size.HasValue ? new RectD(0, 0, size.Value.X, size.Value.Y) : null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var startNode = FindStartNode();
         if (drawingPaint != null && startNode is { Image.Value: not null })
         {
-            renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
+            using var tmpTex = Texture.ForProcessing(startNode.Image.Value.Size, context.ProcessingColorSpace);
 
-            return true;
+            tmpTex.DrawingSurface.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
+            renderOn.Canvas.DrawSurface(tmpTex.DrawingSurface, 0, 0);
         }
-
-        return false;
     }
 
     public override void Dispose()

+ 12 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -12,6 +12,7 @@ using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
@@ -47,7 +48,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     private VecI lastRenderSize = new VecI(0, 0);
 
-    protected internal bool IsDisposed => _isDisposed;
+    public bool IsDisposed => _isDisposed;
     private bool _isDisposed;
 
     private int lastContentCacheHash = -1;
@@ -83,13 +84,18 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected virtual bool CacheChanged(RenderContext context)
     {
-        bool changed = lastRenderSize != context.RenderOutputSize;
+        bool changed = false;
 
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Inputs))
         {
             changed |= inputs.Any(x => x.CacheChanged);
         }
 
+        if (CacheTrigger.HasFlag(CacheTriggerFlags.RenderSize))
+        {
+            changed |= lastRenderSize != context.RenderOutputSize;
+        }
+
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Timeline))
         {
             changed |= lastFrameTime.Frame != context.FrameTime.Frame ||
@@ -117,7 +123,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
         lastContentCacheHash = GetContentCacheHash();
     }
 
-    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action, Func<IInputProperty, bool>? branchCondition = null)
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action,
+        Func<IInputProperty, bool>? branchCondition = null)
     {
         var visited = new HashSet<IReadOnlyNode>();
         var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
@@ -143,6 +150,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 {
                     continue;
                 }
+
                 if (inputProperty.Connection != null)
                 {
                     queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
@@ -613,6 +621,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         hash.Add(GetType());
         hash.Add(DisplayName);
         hash.Add(Position);
+
         foreach (var input in inputs)
         {
             hash.Add(input.GetCacheHash());

+ 3 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -117,29 +117,23 @@ public class NoiseNode : RenderNode
         workingSurface.Canvas.RestoreToCount(saved);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return new RectD(0, 0, 128, 128); 
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var shader = SelectShader();
         if (shader == null)
         {
-            return false;
+            return;
         }
 
         if (paint.Shader != voronoiShader)
         {
             paint?.Shader?.Dispose();
         }
+
         paint.Shader = shader;
         paint.ColorFilter = grayscaleFilter;
         
         RenderNoise(renderOn);
-
-        return true;
     }
 
     private Shader SelectShader()

+ 19 - 24
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -8,7 +8,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("Output")]
-public class OutputNode : Node, IRenderInput, IPreviewRenderable
+public class OutputNode : Node, IRenderInput
 {
     public const string UniqueName = "PixiEditor.Output";
     public const string InputPropertyName = "Background";
@@ -34,33 +34,28 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
 
         Input.Value?.Paint(context, context.RenderSurface);
         lastDocumentSize = context.DocumentSize;
-    }
-
-    RenderInputProperty IRenderInput.Background => Input;
-
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        if (lastDocumentSize == null)
-        {
-            return null;
-        }
 
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
-    }
-
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        if (Input.Value == null)
+        var previews = context.GetPreviewTexturesForNode(Id);
+        if (previews is null) return;
+        foreach (var request in previews)
         {
-            return false;
-        }
+            var texture = request.Texture;
+            if (texture is null) continue;
 
-        int saved = renderOn.Canvas.Save();
-        renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-        Input.Value.Paint(context, renderOn);
+            int saved = texture.DrawingSurface.Canvas.Save();
 
-        renderOn.Canvas.RestoreToCount(saved);
+            VecD scaling = PreviewUtility.CalculateUniformScaling(context.DocumentSize, texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(context.DocumentSize, texture.Size, scaling);
+            texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            var previewCtx =
+                PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
 
-        return true;
+            texture.DrawingSurface.Canvas.Clear();
+            Input.Value?.Paint(previewCtx, texture.DrawingSurface);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
     }
+
+    RenderInputProperty IRenderInput.Background => Input;
 }

+ 57 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -11,7 +11,7 @@ using PixiEditor.ChangeableDocument.Changes.Structure;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
+public abstract class RenderNode : Node, IHighDpiRenderNode
 {
     public RenderOutputProperty Output { get; }
 
@@ -49,10 +49,12 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         DrawingSurface target = surface;
         bool useIntermediate = !AllowHighDpiRendering
                                && context.RenderOutputSize is { X: > 0, Y: > 0 }
-                               && (surface.DeviceClipBounds.Size != context.RenderOutputSize || (RendersInAbsoluteCoordinates && !surface.Canvas.TotalMatrix.IsIdentity));
+                               && (surface.DeviceClipBounds.Size != context.RenderOutputSize ||
+                                   (RendersInAbsoluteCoordinates && !surface.Canvas.TotalMatrix.IsIdentity));
         if (useIntermediate)
         {
-            Texture intermediate = textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
+            Texture intermediate =
+                textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
         }
 
@@ -81,22 +83,66 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
                 surface.Canvas.Restore();
             }
         }
+
+        RenderPreviews(context);
     }
 
     protected abstract void OnPaint(RenderContext context, DrawingSurface surface);
 
-    public virtual RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    protected void RenderPreviews(RenderContext ctx)
     {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+        var previewToRender = ctx.GetPreviewTexturesForNode(Id);
+        if (previewToRender == null || previewToRender.Count == 0)
+            return;
+
+        foreach (var preview in previewToRender)
+        {
+            if (!ShouldRenderPreview(preview.ElementToRender))
+                continue;
+
+            if (preview.Texture == null)
+                continue;
+
+            int saved = preview.Texture.DrawingSurface.Canvas.Save();
+            preview.Texture.DrawingSurface.Canvas.Clear();
+
+            var bounds = GetPreviewBounds(ctx, preview.ElementToRender);
+            if (bounds == null)
+            {
+                bounds = new RectD(0, 0, ctx.RenderOutputSize.X, ctx.RenderOutputSize.Y);
+            }
+
+            VecD scaling = PreviewUtility.CalculateUniformScaling(bounds.Value.Size, preview.Texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(bounds.Value.Size, preview.Texture.Size, scaling);
+            RenderContext adjusted =
+                PreviewUtility.CreatePreviewContext(ctx, scaling, bounds.Value.Size, preview.Texture.Size);
+
+            preview.Texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            preview.Texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            preview.Texture.DrawingSurface.Canvas.Translate((float)-bounds.Value.X, (float)-bounds.Value.Y);
+
+            adjusted.RenderSurface = preview.Texture.DrawingSurface;
+            RenderPreview(preview.Texture.DrawingSurface, adjusted, preview.ElementToRender);
+            preview.Texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
+    }
+
+    protected virtual bool ShouldRenderPreview(string elementToRenderName)
+    {
+        return true;
     }
 
-    public virtual bool RenderPreview(DrawingSurface renderOn, RenderContext context,
+    public virtual RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
+    {
+        return null;
+    }
+
+    public virtual void RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         int saved = renderOn.Canvas.Save();
         OnPaint(context, renderOn);
         renderOn.Canvas.RestoreToCount(saved);
-        return true;
     }
 
     protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
@@ -110,19 +156,18 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         additionalData["AllowHighDpiRendering"] = AllowHighDpiRendering;
     }
 
-    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data,
+        List<IChangeInfo> infos)
     {
         base.DeserializeAdditionalData(target, data, infos);
 
-        if(data.TryGetValue("AllowHighDpiRendering", out var value))
+        if (data.TryGetValue("AllowHighDpiRendering", out var value))
             AllowHighDpiRendering = (bool)value;
     }
 
     public override void Dispose()
     {
         base.Dispose();
-        textureCache.Dispose(); 
+        textureCache.Dispose();
     }
-
-   
 }

+ 4 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -24,7 +24,6 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     private string lastShaderCode;
     private Paint paint;
 
-    private VecI lastDocumentSize;
     private List<Shader> lastCustomImageShaders = new();
 
     private Dictionary<string, (InputProperty prop, UniformValueType valueType)> uniformInputs = new();
@@ -75,7 +74,6 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
             shader = shader.WithUpdatedUniforms(uniforms);
         }
 
-        lastDocumentSize = context.DocumentSize;
         paint.Shader = shader;
     }
 
@@ -207,15 +205,12 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         }
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
+        int saved = renderOn.Canvas.Save();
+        renderOn.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
         OnPaint(context, renderOn);
-        return true;
+        renderOn.Canvas.RestoreToCount(saved);
     }
 
     public override Node CreateCopy()

+ 8 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs

@@ -34,20 +34,23 @@ public class RasterizeShapeNode : RenderNode
 
     public override Node CreateCopy() => new RasterizeShapeNode();
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
         return Data?.Value?.TransformedAABB;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
+    {
+        return Data.Value != null && Data.Value.IsValid();
+    }
+
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var shape = Data.Value;
 
         if (shape == null || !shape.IsValid())
-            return false;
+            return;
 
         shape.RasterizeTransformed(renderOn.Canvas);
-
-        return true;
     }
 }

+ 59 - 75
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -47,8 +47,6 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
     public ChunkyImage? EmbeddedMask { get; set; }
 
-    protected Texture renderedMask;
-
     protected static readonly Paint replacePaint =
         new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
 
@@ -171,11 +169,40 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         VecD sceneSize = GetSceneSize(context.FrameTime);
         //renderTarget.Canvas.ClipRect(new RectD(scenePos - (sceneSize / 2f), sceneSize));
 
+        // Custom shader may modify the actual visible region, so we must force rendering full region
+        if (IsConnectedToCustomShaderNode(output))
+        {
+            renderObjectContext.VisibleDocumentRegion = null;
+        }
+
         Render(renderObjectContext);
 
         renderTarget?.Canvas.RestoreToCount(renderSaved);
     }
 
+    private bool IsConnectedToCustomShaderNode(RenderOutputProperty output)
+    {
+        foreach (var conn in output.Connections)
+        {
+            bool isCustomShader = false;
+            conn.Node.TraverseForwards(x =>
+            {
+                if (x is ICustomShaderNode)
+                {
+                    isCustomShader = true;
+                    return false;
+                }
+
+                return true;
+            });
+
+            if (isCustomShader)
+                return true;
+        }
+
+        return false;
+    }
+
     protected SceneObjectRenderContext CreateSceneContext(RenderContext context, DrawingSurface renderTarget,
         RenderOutputProperty output)
     {
@@ -183,9 +210,13 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
 
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
-            context.FrameTime, context.ChunkResolution, context.RenderOutputSize, context.DocumentSize, renderTarget == context.RenderSurface,
+            context.FrameTime, context.ChunkResolution, context.RenderOutputSize, context.DocumentSize,
+            renderTarget == context.RenderSurface,
             context.ProcessingColorSpace, context.DesiredSamplingOptions, context.Opacity);
         renderObjectContext.FullRerender = context.FullRerender;
+        renderObjectContext.AffectedArea = context.AffectedArea;
+        renderObjectContext.VisibleDocumentRegion = context.VisibleDocumentRegion;
+        renderObjectContext.PreviewTextures = context.PreviewTextures;
         return renderObjectContext;
     }
 
@@ -203,22 +234,12 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
                 surface.Canvas.RestoreToCount(layer);
             }
-            else if (EmbeddedMask != null)
+            else
             {
-                if (context.FullRerender)
-                {
-                    EmbeddedMask.DrawMostUpToDateRegionOn(
-                        new RectI(0, 0, EmbeddedMask.LatestSize.X, EmbeddedMask.LatestSize.Y),
-                        ChunkResolution.Full,
-                        surface, VecI.Zero, maskPaint);
-                }
-                else if (renderedMask != null)
-                {
-                    int saved = surface.Canvas.Save();
-                    surface.Canvas.Scale((float)renderResolution.Multiplier());
-                    surface.Canvas.DrawSurface(renderedMask.DrawingSurface, 0, 0, maskPaint);
-                    surface.Canvas.RestoreToCount(saved);
-                }
+                EmbeddedMask?.DrawMostUpToDateRegionOn(
+                    new RectI(0, 0, EmbeddedMask.LatestSize.X, EmbeddedMask.LatestSize.Y),
+                    context.ChunkResolution,
+                    surface, VecI.Zero, maskPaint, drawPaintOnEmpty: true);
             }
         }
     }
@@ -229,48 +250,6 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
             ClipToPreviousMember ? 1 : 0);
     }
 
-    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
-        ColorSpace processingColorSpace)
-    {
-        RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, processingColorSpace, ref renderedMask);
-    }
-
-    protected void RenderChunkyImageChunk(VecI chunkPos, ChunkResolution resolution, ChunkyImage img,
-        int textureId, ColorSpace processingColorSpace,
-        ref Texture? renderSurface)
-    {
-        if (img is null)
-        {
-            return;
-        }
-
-        VecI targetSize = img.LatestSize;
-
-        if (targetSize.X <= 0 || targetSize.Y <= 0)
-        {
-            return;
-        }
-
-        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
-        renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
-
-        int saved = renderSurface.DrawingSurface.Canvas.Save();
-
-        if (!img.DrawMostUpToDateChunkOn(
-                chunkPos,
-                ChunkResolution.Full,
-                renderSurface.DrawingSurface,
-                chunkPos * ChunkResolution.Full.PixelSize(),
-                replacePaint))
-        {
-            var chunkSize = ChunkResolution.Full.PixelSize();
-            renderSurface.DrawingSurface.Canvas.DrawRect(new RectD(chunkPos * chunkSize, new VecD(chunkSize)),
-                clearPaint);
-        }
-
-        renderSurface.DrawingSurface.Canvas.RestoreToCount(saved);
-    }
-
     protected void ApplyRasterClip(DrawingSurface toClip, DrawingSurface clipSource)
     {
         if (ClipToPreviousMember && Background.Value != null)
@@ -335,40 +314,45 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
-        if (elementFor == nameof(EmbeddedMask) && EmbeddedMask != null)
+        if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return new RectD(VecD.Zero, EmbeddedMask.LatestSize);
+            return true;
         }
 
-        return null;
+        return EmbeddedMask != null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
-        string elementToRenderName)
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
     {
-        if (elementToRenderName != nameof(EmbeddedMask))
-        {
-            return false;
-        }
+        return EmbeddedMask is null
+            ? null
+            : new RectD(0, 0, EmbeddedMask.LatestSize.X, EmbeddedMask.LatestSize.Y);
+    }
 
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context,
+        string elementToRenderName)
+    {
         var img = EmbeddedMask;
 
         if (img is null)
         {
-            return false;
+            return;
         }
 
-        renderOn.Canvas.DrawSurface(renderedMask.DrawingSurface, VecI.Zero, maskPreviewPaint);
-
-        return true;
+        int saved = renderOn.Canvas.Save();
+        renderOn.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
+        img.DrawMostUpToDateRegionOn(
+            new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
+            context.ChunkResolution,
+            renderOn, VecI.Zero, maskPreviewPaint, drawPaintOnEmpty: true);
+        renderOn.Canvas.RestoreToCount(saved);
     }
 
     public override void Dispose()
     {
         base.Dispose();
-        renderedMask?.Dispose();
         EmbeddedMask?.Dispose();
         Output.Value = null;
         maskPaint.Dispose();

+ 0 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs

@@ -69,15 +69,4 @@ public class TileNode : RenderNode
     {
         return new TileNode();
     }
-
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return null;
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        return false;
-    }
-
 }

+ 25 - 17
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -90,31 +90,35 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         Rasterize(workingSurface, paint);
     }
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
-        if (elementFor == nameof(EmbeddedMask))
+        if(RenderableShapeData == null)
         {
-            base.GetPreviewBounds(frame, elementFor);
-        }
-        else
-        {
-            return RenderableShapeData?.TransformedVisualAABB;
+            return false;
         }
 
-        return null;
+        VecI tightBoundsSize = (VecI)RenderableShapeData.TransformedVisualAABB.Size;
+
+        VecI translation = new VecI(
+            (int)Math.Max(RenderableShapeData.TransformedAABB.TopLeft.X, 0),
+            (int)Math.Max(RenderableShapeData.TransformedAABB.TopLeft.Y, 0));
+
+        VecI size = tightBoundsSize + translation;
+        return size.X > 0 && size.Y > 0;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return base.RenderPreview(renderOn, context, elementToRenderName);
+            base.RenderPreview(renderOn, context, elementToRenderName);
+            return;
         }
 
         if (RenderableShapeData == null)
         {
-            return false;
+            return;
         }
 
         using var paint = new Paint();
@@ -129,16 +133,20 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
         if (size.X == 0 || size.Y == 0)
         {
-            return false;
+            return;
         }
 
-
-        int savedCount = renderOn.Canvas.Save();
-        renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
         Rasterize(renderOn, paint);
-        renderOn.Canvas.RestoreToCount(savedCount);
+    }
+
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
+    {
+        if (elementToRenderName == nameof(EmbeddedMask))
+        {
+            return base.GetPreviewBounds(ctx, elementToRenderName);
+        }
 
-        return true;
+        return GetTightBounds(ctx.FrameTime);
     }
 
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 
 [NodeInfo("CustomOutput")]
-public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
+public class CustomOutputNode : Node, IRenderInput
 {
     public const string OutputNamePropertyName = "OutputName";
     public const string IsDefaultExportPropertyName = "IsDefaultExport";

+ 12 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -2,10 +2,12 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
@@ -52,6 +54,8 @@ public static class FloodFillHelper
 
         int chunkSize = ChunkResolution.Full.PixelSize();
 
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
         FloodFillChunkCache cache = CreateCache(membersToFloodFill, document, frame);
 
         VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
@@ -210,10 +214,12 @@ public static class FloodFillHelper
         byte[] pixelStates = new byte[chunkSize * chunkSize];
         DrawSelection(pixelStates, selection, globalSelectionBounds, chunkPos, chunkSize);
 
-        using var refPixmap = referenceChunk.Surface.DrawingSurface.PeekPixels();
+        using var refPixmap = referenceChunk.Surface.PeekPixels();
         Half* refArray = (Half*)refPixmap.GetPixels();
 
-        using var drawPixmap = drawingChunk.Surface.DrawingSurface.PeekPixels();
+        Surface cpuSurface = Surface.ForProcessing(new VecI(chunkSize), referenceChunk.Surface.ColorSpace);
+        cpuSurface.DrawingSurface.Canvas.DrawSurface(drawingChunk.Surface.DrawingSurface, 0, 0);
+        using var drawPixmap = cpuSurface.PeekPixels();
         Half* drawArray = (Half*)drawPixmap.GetPixels();
 
         Stack<VecI> toVisit = new();
@@ -241,6 +247,10 @@ public static class FloodFillHelper
                 toVisit.Push(new(curPos.X, curPos.Y + 1));
         }
 
+        using Paint replacePaint = new Paint();
+        replacePaint.BlendMode = BlendMode.Src;
+        drawingChunk.Surface.DrawingSurface.Canvas.DrawSurface(cpuSurface.DrawingSurface, 0, 0, replacePaint);
+
         return pixelStates;
     }
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -57,7 +57,7 @@ internal class FloodFill_Change : Change
 
         foreach (var (chunkPos, chunk) in floodFilledChunks)
         {
-            image.EnqueueDrawImage(chunkPos * ChunkyImage.FullChunkSize, chunk.Surface, null, false);
+            image.EnqueueDrawTexture(chunkPos * ChunkyImage.FullChunkSize, chunk.Surface, null, false);
         }
         var affArea = image.FindAffectedArea();
         chunkStorage = new CommittedChunkStorage(image, affArea.Chunks);

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs

@@ -1,5 +1,6 @@
 using System.Collections;
 using ChunkyImageLib.Operations;
+using Drawie.Backend.Core.Bridge;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 using Drawie.Backend.Core.ColorsImpl;
@@ -257,6 +258,7 @@ internal class MagicWandHelper
         VecI pos,
         ColorBounds bounds, Lines lines)
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         if (!bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixelPrecise(pos)))
         {
             return null;
@@ -264,7 +266,7 @@ internal class MagicWandHelper
 
         bool[] pixelVisitedStates = new bool[chunkSize * chunkSize];
 
-        using var refPixmap = referenceChunk.Surface.DrawingSurface.PeekPixels();
+        using var refPixmap = referenceChunk.Surface.PeekPixels();
         Half* refArray = (Half*)refPixmap.GetPixels();
 
         Stack<VecI> toVisit = new();

+ 2 - 1
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using Drawie.Backend.Core.Bridge;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -430,7 +431,7 @@ public class DocumentChangeTracker : IDisposable
         if (running)
             throw new InvalidOperationException("Already currently processing");
         running = true;
-        var result = await Task.Run(() => ProcessActionList(actions)).ConfigureAwait(true);
+        var result = await DrawingBackendApi.Current.RenderingDispatcher.InvokeAsync(() => ProcessActionList(actions));
         running = false;
         return result;
     }

+ 0 - 17
src/PixiEditor.ChangeableDocument/Helpers/PreviewUtils.cs

@@ -1,17 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using Drawie.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Helpers;
-
-public static class PreviewUtils
-{
-    public static RectD? FindPreviewBounds(IOutputProperty? connectionProperty, int frame, string elementToRenderName)
-    {
-        if (connectionProperty is { Node: IPreviewRenderable previousPreview })
-        {
-            return previousPreview.GetPreviewBounds(frame, elementToRenderName);
-        }
-
-        return null;
-    }
-}

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

@@ -17,11 +17,9 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 
 namespace PixiEditor.ChangeableDocument.Rendering;
 
-public class DocumentRenderer : IPreviewRenderable, IDisposable
+public class DocumentRenderer : IDisposable
 {
-    private Queue<RenderRequest> renderRequests = new();
     private Texture renderTexture;
-    private int lastExecutedGraphFrame = -1;
 
     public DocumentRenderer(IReadOnlyDocument document)
     {
@@ -31,7 +29,6 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
     private IReadOnlyDocument Document { get; }
     public bool IsBusy { get; private set; }
 
-    private bool isExecuting = false;
 
     public void UpdateChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime)
     {
@@ -126,20 +123,6 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         IsBusy = false;
     }
 
-    public async Task<bool> RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn,
-        RenderContext context,
-        string elementToRenderName)
-    {
-        if (previewRenderable is Node { IsDisposed: true }) return false;
-        TaskCompletionSource<bool> tcs = new();
-        RenderRequest request = new(tcs, context, renderOn, previewRenderable, elementToRenderName);
-
-        renderRequests.Enqueue(request);
-        ExecuteRenderRequests(context.FrameTime);
-
-        return await tcs.Task;
-    }
-
     public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(IReadOnlyNodeGraph fullGraph)
     {
         return ConstructMembersOnlyGraph(null, fullGraph);
@@ -193,27 +176,6 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         return membersOnlyGraph;
     }
 
-    RectD? IPreviewRenderable.GetPreviewBounds(int frame, string elementNameToRender = "") =>
-        new(0, 0, Document.Size.X, Document.Size.Y);
-
-    bool IPreviewRenderable.RenderPreview(DrawingSurface renderOn, RenderContext context,
-        string elementToRenderName)
-    {
-        IsBusy = true;
-
-        renderOn.Canvas.Clear();
-        int savedCount = renderOn.Canvas.Save();
-        renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-        context.RenderSurface = renderOn;
-        Document.NodeGraph.Execute(context);
-        lastExecutedGraphFrame = context.FrameTime.Frame;
-        renderOn.Canvas.RestoreToCount(savedCount);
-
-        IsBusy = false;
-
-        return true;
-    }
-
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize,
         string? customOutput = null)
     {
@@ -270,52 +232,9 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         renderTexture.DrawingSurface.Canvas.Restore();
         toRenderOn.Canvas.Restore();
 
-        lastExecutedGraphFrame = frameTime.Frame;
-
         IsBusy = false;
     }
 
-    private void ExecuteRenderRequests(KeyFrameTime frameTime)
-    {
-        if (isExecuting) return;
-
-        isExecuting = true;
-        using var ctx = DrawingBackendApi.Current?.RenderingDispatcher.EnsureContext();
-
-        while (renderRequests.Count > 0)
-        {
-            RenderRequest request = renderRequests.Dequeue();
-
-            if (frameTime.Frame != lastExecutedGraphFrame && request.PreviewRenderable != this)
-            {
-                using Texture executeSurface = Texture.ForDisplay(new VecI(1));
-                RenderDocument(executeSurface.DrawingSurface, frameTime, VecI.One);
-            }
-
-            try
-            {
-                bool result = true;
-                if (request.PreviewRenderable != null)
-                {
-                    result = request.PreviewRenderable.RenderPreview(request.RenderOn, request.Context,
-                        request.ElementToRenderName);
-                }
-                else if (request.NodeGraph != null)
-                {
-                    request.NodeGraph.Execute(request.Context);
-                }
-
-                request.TaskCompletionSource.SetResult(result);
-            }
-            catch (Exception e)
-            {
-                request.TaskCompletionSource.SetException(e);
-            }
-        }
-
-        isExecuting = false;
-    }
-
     private static IInputProperty GetTargetInput(IInputProperty? input,
         IReadOnlyNodeGraph sourceGraph,
         NodeGraph membersOnlyGraph,
@@ -383,41 +302,5 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
     {
         renderTexture?.Dispose();
         renderTexture = null;
-
-        foreach (var request in renderRequests)
-        {
-            if (request.TaskCompletionSource == null) continue;
-
-            request.TaskCompletionSource.TrySetCanceled();
-        }
-    }
-}
-
-public struct RenderRequest
-{
-    public RenderContext Context { get; set; }
-    public DrawingSurface RenderOn { get; set; }
-    public IReadOnlyNodeGraph? NodeGraph { get; set; } // TODO: Implement async rendering for stuff other than previews
-    public IPreviewRenderable? PreviewRenderable { get; set; }
-    public string ElementToRenderName { get; set; }
-    public TaskCompletionSource<bool> TaskCompletionSource { get; set; }
-
-    public RenderRequest(TaskCompletionSource<bool> completionSource, RenderContext context, DrawingSurface renderOn,
-        IReadOnlyNodeGraph nodeGraph)
-    {
-        TaskCompletionSource = completionSource;
-        Context = context;
-        RenderOn = renderOn;
-        NodeGraph = nodeGraph;
-    }
-
-    public RenderRequest(TaskCompletionSource<bool> completionSource, RenderContext context, DrawingSurface renderOn,
-        IPreviewRenderable previewRenderable, string elementToRenderName)
-    {
-        TaskCompletionSource = completionSource;
-        Context = context;
-        RenderOn = renderOn;
-        PreviewRenderable = previewRenderable;
-        ElementToRenderName = elementToRenderName;
     }
 }

+ 43 - 0
src/PixiEditor.ChangeableDocument/Rendering/PreviewRenderRequest.cs

@@ -0,0 +1,43 @@
+using Drawie.Backend.Core;
+
+namespace PixiEditor.ChangeableDocument.Rendering;
+
+public record struct PreviewRenderRequest
+{
+    public Texture? Texture
+    {
+        get
+        {
+            if (!accessedTexture)
+            {
+                texture = textureCreateFunc(true);
+                accessedTexture = true;
+            }
+
+            return texture;
+        }
+    }
+    public string? ElementToRender { get; set; }
+    public Action TextureUpdatedAction { get; set; }
+
+    private Func<bool, Texture?> textureCreateFunc;
+    private Texture? texture;
+    private bool accessedTexture = false;
+
+    public PreviewRenderRequest(Func<bool, Texture?> textureCreateFunc, Action textureUpdatedAction, string? elementToRender = null)
+    {
+        this.textureCreateFunc = textureCreateFunc;
+        TextureUpdatedAction = textureUpdatedAction;
+        ElementToRender = elementToRender;
+    }
+
+    public void InvokeTextureUpdated()
+    {
+        TextureUpdatedAction?.Invoke();
+    }
+
+    public Texture? GetTextureCached()
+    {
+        return textureCreateFunc(false);
+    }
+}

+ 48 - 0
src/PixiEditor.ChangeableDocument/Rendering/PreviewUtility.cs

@@ -0,0 +1,48 @@
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Rendering;
+
+public static class PreviewUtility
+{
+    public static ChunkResolution CalculateResolution(VecD size, VecD textureSize)
+    {
+        VecD densityVec = size.Divide(textureSize);
+        double density = Math.Min(densityVec.X, densityVec.Y);
+        return density switch
+        {
+            > 8.01 => ChunkResolution.Eighth,
+            > 4.01 => ChunkResolution.Quarter,
+            > 2.01 => ChunkResolution.Half,
+            _ => ChunkResolution.Full
+        };
+    }
+
+    public static VecD CalculateUniformScaling(VecD originalSize, VecD targetSize)
+    {
+        if (originalSize.X == 0 || originalSize.Y == 0)
+            return new VecD(1);
+
+        VecD scale = targetSize.Divide(originalSize);
+        double uniformScale = Math.Min(scale.X, scale.Y);
+        return new VecD(uniformScale, uniformScale);
+    }
+
+    public static VecD CalculateCenteringOffset(VecD originalSize, VecD targetSize, VecD scaling)
+    {
+        if (originalSize.X == 0 || originalSize.Y == 0)
+            return VecD.Zero;
+
+        VecD scaledOriginal = originalSize.Multiply(scaling);
+        return (targetSize - scaledOriginal) / 2;
+    }
+
+    public static RenderContext CreatePreviewContext(RenderContext ctx, VecD scaling, VecD renderSize, VecD textureSize)
+    {
+        var clone = ctx.Clone();
+        clone.ChunkResolution = CalculateResolution(renderSize, textureSize);
+        clone.DesiredSamplingOptions = scaling.X > 1 ? SamplingOptions.Default : SamplingOptions.Bilinear;
+
+        return clone;
+    }
+}

+ 19 - 3
src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using Drawie.Backend.Core;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
@@ -13,6 +14,7 @@ public class RenderContext
 
     public KeyFrameTime FrameTime { get; }
     public ChunkResolution ChunkResolution { get; set; }
+    public RectI? VisibleDocumentRegion { get; set; } = null;
     public SamplingOptions DesiredSamplingOptions { get; set; } = SamplingOptions.Default;
     public VecI RenderOutputSize { get; set; }
 
@@ -21,7 +23,9 @@ public class RenderContext
     public bool FullRerender { get; set; } = false;
     
     public ColorSpace ProcessingColorSpace { get; set; }
-    public string? TargetOutput { get; set; }   
+    public string? TargetOutput { get; set; }
+    public AffectedArea AffectedArea { get; set; }
+    public Dictionary<Guid, List<PreviewRenderRequest>>? PreviewTextures { get; set; }
 
 
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
@@ -37,6 +41,15 @@ public class RenderContext
         DesiredSamplingOptions = desiredSampling;
     }
 
+    public List<PreviewRenderRequest>? GetPreviewTexturesForNode(Guid id)
+    {
+        if (PreviewTextures is null)
+            return null;
+        PreviewTextures.TryGetValue(id, out List<PreviewRenderRequest> requests);
+        PreviewTextures.Remove(id);
+        return requests;
+    }
+
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)
     {
         return blendMode switch
@@ -63,12 +76,15 @@ public class RenderContext
         };
     }
 
-    public RenderContext Clone()
+    public virtual RenderContext Clone()
     {
         return new RenderContext(RenderSurface, FrameTime, ChunkResolution, RenderOutputSize, DocumentSize, ProcessingColorSpace, DesiredSamplingOptions, Opacity)
         {
             FullRerender = FullRerender,
             TargetOutput = TargetOutput,
+            AffectedArea = AffectedArea,
+            PreviewTextures = PreviewTextures,
+            VisibleDocumentRegion = VisibleDocumentRegion
         };
     }
 }

+ 27 - 0
src/PixiEditor/Helpers/Converters/ResultPreviewIsPresentConverter.cs

@@ -0,0 +1,27 @@
+using System.Globalization;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class ResultPreviewIsPresentConverter : SingleInstanceMultiValueConverter<ResultPreviewIsPresentConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is TexturePreview preview)
+        {
+            return preview.Preview != null;
+        }
+
+        return false;
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (values[0] is TexturePreview preview)
+        {
+            return preview.Preview != null;
+        }
+
+        return false;
+    }
+}

+ 35 - 25
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -1,11 +1,13 @@
 using System.Diagnostics;
 using Avalonia.Threading;
+using Drawie.Backend.Core;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using Drawie.Backend.Core.Bridge;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentPassthroughActions;
@@ -22,7 +24,6 @@ internal class ActionAccumulator
     private IDocument document;
     private DocumentInternalParts internals;
 
-    private CanvasUpdater canvasUpdater;
     private MemberPreviewUpdater previewUpdater;
 
     private bool isChangeBlockActive = false;
@@ -32,7 +33,6 @@ internal class ActionAccumulator
         this.document = doc;
         this.internals = internals;
 
-        canvasUpdater = new(doc, internals);
         previewUpdater = new(doc, internals);
     }
 
@@ -84,7 +84,7 @@ internal class ActionAccumulator
         TryExecuteAccumulatedActions();
     }
 
-    internal void TryExecuteAccumulatedActions()
+    internal async Task TryExecuteAccumulatedActions()
     {
         if (executing || queuedActions.Count == 0)
             return;
@@ -112,7 +112,7 @@ internal class ActionAccumulator
                 }
                 else
                 {
-                    changes = internals.Tracker.ProcessActionsSync(toExecute);
+                    changes = await internals.Tracker.ProcessActions(toExecute);
                 }
 
                 List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
@@ -123,6 +123,8 @@ internal class ActionAccumulator
                     toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
                 bool refreshPreviewsRequest =
                     toExecute.Any(static action => action.action is RefreshPreviews_PassthroughAction);
+                bool refreshPreviewRequest =
+                    toExecute.Any(static action => action.action is RefreshPreview_PassthroughAction);
                 bool changeFrameRequest =
                     toExecute.Any(static action => action.action is SetActiveFrame_PassthroughAction);
 
@@ -134,44 +136,41 @@ internal class ActionAccumulator
                 if (undoBoundaryPassed)
                     internals.Updater.AfterUndoBoundaryPassed();
 
-
                 var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime,
                     internals.Tracker,
                     optimizedChanges, refreshPreviewsRequest);
 
-                if (DrawingBackendApi.Current.IsHardwareAccelerated && !allPassthrough)
-                {
-                    canvasUpdater.UpdateGatheredChunksSync(affectedAreas,
-                        undoBoundaryPassed || viewportRefreshRequest);
-                }
-                /*else
-                {
-                    await canvasUpdater.UpdateGatheredChunks(affectedAreas,
-                        undoBoundaryPassed || viewportRefreshRequest);
-                }*/
-
                 bool previewsDisabled = PixiEditorSettings.Performance.DisablePreviews.Value;
+                bool updateDelayed = undoBoundaryPassed || viewportRefreshRequest || changeFrameRequest ||
+                                     document.SizeBindable.LongestAxis <= LiveUpdatePerformanceThreshold;
+
+                Dictionary<Guid, List<PreviewRenderRequest>>? previewTextures = null;
 
                 if (!previewsDisabled)
                 {
-                    if (undoBoundaryPassed || viewportRefreshRequest || changeFrameRequest ||
+                    if (undoBoundaryPassed || viewportRefreshRequest || refreshPreviewsRequest ||
+                        refreshPreviewRequest || changeFrameRequest ||
                         document.SizeBindable.LongestAxis <= LiveUpdatePerformanceThreshold)
                     {
-                        previewUpdater.UpdatePreviews(
+                        previewTextures = previewUpdater.GatherPreviewsToUpdate(
                             affectedAreas.ChangedMembers,
                             affectedAreas.ChangedMasks,
                             affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames,
                             affectedAreas.IgnoreAnimationPreviews,
-                            undoBoundaryPassed || refreshPreviewsRequest);
+                            undoBoundaryPassed || refreshPreviewsRequest || refreshPreviewRequest);
                     }
                 }
 
-                // force refresh viewports for better responsiveness
-                foreach (var (_, value) in internals.State.Viewports)
-                {
-                    if (!value.Delayed)
-                        value.InvalidateVisual();
-                }
+                List<Action>? updatePreviewActions = previewTextures?.Values
+                    .Select(x => x.Select(r => r.TextureUpdatedAction))
+                    .SelectMany(x => x).ToList();
+
+                bool immediateRender = affectedAreas.MainImageArea.Chunks.Count > 0;
+
+                await document.SceneRenderer.RenderAsync(internals.State.Viewports, affectedAreas.MainImageArea,
+                    !previewsDisabled && updateDelayed, previewTextures, immediateRender);
+
+                NotifyUpdatedPreviews(updatePreviewActions);
             }
         }
         catch (Exception e)
@@ -192,6 +191,17 @@ internal class ActionAccumulator
         executing = false;
     }
 
+    private static void NotifyUpdatedPreviews(List<Action>? updatePreviewActions)
+    {
+        if (updatePreviewActions != null)
+        {
+            foreach (var action in updatePreviewActions)
+            {
+                action();
+            }
+        }
+    }
+
     private const int LiveUpdatePerformanceThreshold = 2048;
 
     private bool AreAllPassthrough(List<(ActionSource, IAction)> actions)

+ 7 - 0
src/PixiEditor/Models/DocumentPassthroughActions/RefreshPreview_PassthroughAction.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.Models.Position;
+
+namespace PixiEditor.Models.DocumentPassthroughActions;
+
+internal record class RefreshPreview_PassthroughAction(Guid Id, Guid? SubId = null, string ElementToRender = null) : IAction, IChangeInfo;

+ 2 - 1
src/PixiEditor/Models/Handlers/ICelHandler.cs

@@ -1,12 +1,13 @@
 using ChunkyImageLib;
 using Drawie.Backend.Core;
 using PixiEditor.Models.Rendering;
+using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Models.Handlers;
 
 internal interface ICelHandler : IDisposable
 {
-    public PreviewPainter? PreviewPainter { get; set; }
+    TexturePreview? PreviewTexture { get; set; }
     public int StartFrameBindable { get; }
     public int DurationBindable { get; }
     public bool IsSelected { get; set; }

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

@@ -33,7 +33,6 @@ internal interface IDocument : IHandler, Extensions.CommonApi.Documents.IDocumen
     public VectorPath SelectionPathBindable { get; }
     public INodeGraphHandler NodeGraphHandler { get; }
     public DocumentStructureModule StructureHelper { get; }
-    public PreviewPainter? PreviewPainter { get; set; }
     public bool AllChangesSaved { get; }
     public string CoordinatesString { get; set; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
@@ -50,7 +49,8 @@ internal interface IDocument : IHandler, Extensions.CommonApi.Documents.IDocumen
     public DocumentRenderer Renderer { get; }
     public ISnappingHandler SnappingHandler { get; }
     public IReadOnlyCollection<Guid> SelectedMembers { get; }
-    public PreviewPainter? MiniPreviewPainter { get; set; }
+    public Dictionary<Guid, Texture> SceneTextures { get; }
+    public SceneRenderer SceneRenderer { get; }
     public void RemoveSoftSelectedMember(IStructureMemberHandler member);
     public void ClearSoftSelectedMembers();
     public void AddSoftSelectedMember(IStructureMemberHandler member);

+ 3 - 7
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -1,14 +1,10 @@
-using System.Collections.ObjectModel;
-using System.ComponentModel;
+using System.ComponentModel;
 using Avalonia;
 using Avalonia.Media;
-using ChunkyImageLib;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
-using Drawie.Backend.Core;
-using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.Handlers;
@@ -22,7 +18,7 @@ public interface INodeHandler : INotifyPropertyChanged, IDisposable
     public NodeMetadata Metadata { get; set; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
-    public PreviewPainter? ResultPainter { get; set; }
+    public TexturePreview? Preview { get; set; }
     public VecD PositionBindable { get; set; }
     public Rect UiSize { get; set; }
     public bool IsNodeSelected { get; set; }

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

@@ -7,6 +7,7 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Rendering;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Document;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 namespace PixiEditor.Models.Handlers;
@@ -14,8 +15,8 @@ namespace PixiEditor.Models.Handlers;
 internal interface IStructureMemberHandler : INodeHandler
 {
     public bool HasMaskBindable { get; }
-    public PreviewPainter? MaskPreviewPainter { get; set; }
-    public PreviewPainter? PreviewPainter { get; set; }
+    public TexturePreview? MaskPreview { get; set; }
+    public TexturePreview? Preview { get; set; }
     public bool MaskIsVisibleBindable { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public float OpacityBindable { get; set; }

+ 9 - 1
src/PixiEditor/Models/Position/ViewportInfo.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
 
 namespace PixiEditor.Models.Position;
@@ -11,8 +12,15 @@ internal readonly record struct ViewportInfo(
     double Angle,
     VecD Center,
     VecD RealDimensions,
+    Matrix3X3 Transform,
+    RectI? VisibleDocumentRegion,
+    string RenderOutput,
+    SamplingOptions Sampling,
     VecD Dimensions,
     ChunkResolution Resolution,
     Guid Id,
     bool Delayed,
-    Action InvalidateVisual);
+    bool IsScene,
+    Action InvalidateVisual)
+{
+}

+ 40 - 2
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -52,6 +52,7 @@ internal class AffectedAreasGatherer
             AddWholeCanvasToEveryImagePreview(false);
             AddWholeCanvasToEveryMaskPreview();
             AddAllNodesToImagePreviews();
+            AddAllKeyFrames();
             return;
         }
 
@@ -119,6 +120,7 @@ internal class AffectedAreasGatherer
                 case StructureMemberMask_ChangeInfo info:
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToImagePreviews(info.Id, true);
+                    AddToMaskPreview(info.Id);
                     AddToNodePreviews(info.Id);
                     break;
                 case StructureMemberBlendMode_ChangeInfo info:
@@ -209,15 +211,51 @@ internal class AffectedAreasGatherer
                     AddWholeCanvasToEveryImagePreview(false);
                     AddWholeCanvasToEveryMaskPreview();
                     break;
+                case RefreshPreview_PassthroughAction info:
+                    ProcessRefreshPreview(info);
+                    break;
             }
         }
     }
 
+    private void ProcessRefreshPreview(RefreshPreview_PassthroughAction info)
+    {
+        if (info.SubId == null)
+        {
+            if (info.ElementToRender == nameof(StructureNode.EmbeddedMask))
+            {
+                AddToMaskPreview(info.Id);
+            }
+            else
+            {
+                AddToImagePreviews(info.Id);
+                AddToNodePreviews(info.Id);
+            }
+        }
+        else
+        {
+            AddKeyFrame(info.SubId.Value);
+        }
+    }
+
+    private void AddAllKeyFrames()
+    {
+        ChangedKeyFrames ??= new HashSet<Guid>();
+        tracker.Document.ForEveryReadonlyMember((member) =>
+        {
+            foreach (var keyFrame in member.KeyFrames)
+            {
+                ChangedKeyFrames.Add(keyFrame.KeyFrameGuid);
+            }
+
+            ChangedKeyFrames.Add(member.Id);
+        });
+    }
+
     private void AddKeyFrame(Guid infoKeyFrameId)
     {
         ChangedKeyFrames ??= new HashSet<Guid>();
-        if (!ChangedKeyFrames.Contains(infoKeyFrameId))
-            ChangedKeyFrames.Add(infoKeyFrameId);
+        ChangedKeyFrames.Add(infoKeyFrameId);
     }
 
     private void AddToNodePreviews(Guid nodeId)

+ 3 - 3
src/PixiEditor/Models/Rendering/AnimationPreviewRenderer.cs

@@ -8,7 +8,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.Models.Rendering;
 
-internal class AnimationKeyFramePreviewRenderer(DocumentInternalParts internals) : IPreviewRenderable
+internal class AnimationKeyFramePreviewRenderer(DocumentInternalParts internals)
 {
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
@@ -22,7 +22,7 @@ internal class AnimationKeyFramePreviewRenderer(DocumentInternalParts internals)
         return null;
     }
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    /*public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if (internals.Tracker.Document.AnimationData.TryFindKeyFrame(
                 Guid.Parse(elementToRenderName),
@@ -38,5 +38,5 @@ internal class AnimationKeyFramePreviewRenderer(DocumentInternalParts internals)
         }
         
         return false;
-    }
+    }*/
 }

+ 0 - 210
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -1,210 +0,0 @@
-using ChunkyImageLib.DataHolders;
-using ChunkyImageLib.Operations;
-using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.ColorsImpl;
-using Drawie.Backend.Core.Surfaces;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Handlers;
-using Drawie.Numerics;
-
-namespace PixiEditor.Models.Rendering;
-#nullable enable
-internal class CanvasUpdater
-{
-    private readonly IDocument doc;
-    private readonly DocumentInternalParts internals;
-
-    private Dictionary<int, Texture> renderedFramesCache = new();
-    private int lastRenderedFrameNumber = -1;
-    private int lastOnionKeyFrames = -1;
-    private double lastOnionOpacity = -1;
-
-    /// <summary>
-    /// Affected chunks that have not been rerendered yet.
-    /// </summary>
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> affectedAndNonRerenderedChunks = new()
-    {
-        [ChunkResolution.Full] = new(),
-        [ChunkResolution.Half] = new(),
-        [ChunkResolution.Quarter] = new(),
-        [ChunkResolution.Eighth] = new()
-    };
-
-    /// <summary>
-    /// Affected chunks that have not been rerendered yet.
-    /// Doesn't include chunks that were affected after the last time rerenderDelayed was true.
-    /// </summary>
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> nonRerenderedChunksAffectedBeforeLastRerenderDelayed =
-        new()
-        {
-            [ChunkResolution.Full] = new(),
-            [ChunkResolution.Half] = new(),
-            [ChunkResolution.Quarter] = new(),
-            [ChunkResolution.Eighth] = new()
-        };
-
-
-    public CanvasUpdater(IDocument doc, DocumentInternalParts internals)
-    {
-        this.doc = doc;
-        this.internals = internals;
-    }
-
-    /// <summary>
-    /// Don't call this outside ActionAccumulator
-    /// </summary>
-    public async Task UpdateGatheredChunks
-        (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
-    {
-        await Task.Run(() => Render(chunkGatherer, rerenderDelayed)).ConfigureAwait(true);
-    }
-
-    /// <summary>
-    /// Don't call this outside ActionAccumulator
-    /// </summary>
-    public void UpdateGatheredChunksSync
-        (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
-    {
-        Render(chunkGatherer, rerenderDelayed);
-    }
-
-    private Dictionary<ChunkResolution, HashSet<VecI>> FindChunksVisibleOnViewports(bool onDelayed, bool all)
-    {
-        Dictionary<ChunkResolution, HashSet<VecI>> chunks = new()
-        {
-            [ChunkResolution.Full] = new(),
-            [ChunkResolution.Half] = new(),
-            [ChunkResolution.Quarter] = new(),
-            [ChunkResolution.Eighth] = new()
-        };
-        foreach (var (_, viewport) in internals.State.Viewports)
-        {
-            if (onDelayed != viewport.Delayed && !all)
-                continue;
-
-            var viewportChunks = OperationHelper.FindChunksTouchingRectangle(
-                viewport.Center,
-                viewport.Dimensions,
-                -viewport.Angle,
-                ChunkResolution.Full.PixelSize());
-            chunks[viewport.Resolution].UnionWith(viewportChunks);
-        }
-
-        return chunks;
-    }
-
-    private Dictionary<ChunkResolution, HashSet<VecI>> FindGlobalChunksToRerender(AffectedAreasGatherer areasGatherer,
-        bool renderDelayed)
-    {
-        // find all affected non rerendered chunks
-        var chunksToRerender = new Dictionary<ChunkResolution, HashSet<VecI>>();
-        foreach (var (res, stored) in affectedAndNonRerenderedChunks)
-        {
-            chunksToRerender[res] = new HashSet<VecI>(stored);
-            chunksToRerender[res].UnionWith(areasGatherer.MainImageArea.Chunks);
-        }
-
-        // find all chunks that would need to be rerendered if affected
-        var chunksToMaybeRerender = FindChunksVisibleOnViewports(false, renderDelayed);
-        if (!renderDelayed)
-        {
-            var chunksOnDelayedViewports = FindChunksVisibleOnViewports(true, false);
-            foreach (var (res, stored) in nonRerenderedChunksAffectedBeforeLastRerenderDelayed)
-            {
-                chunksOnDelayedViewports[res].IntersectWith(stored);
-                chunksToMaybeRerender[res].UnionWith(chunksOnDelayedViewports[res]);
-            }
-        }
-
-        // find affected chunks that need to be rerendered right now
-        foreach (var (res, toRerender) in chunksToRerender)
-        {
-            toRerender.IntersectWith(chunksToMaybeRerender[res]);
-        }
-
-        return chunksToRerender;
-    }
-
-    private void UpdateAffectedNonRerenderedChunks(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender,
-        AffectedArea chunkGathererAffectedArea)
-    {
-        if (chunkGathererAffectedArea.Chunks.Count > 0)
-        {
-            foreach (var (res, chunks) in chunksToRerender)
-            {
-                affectedAndNonRerenderedChunks[res].UnionWith(chunkGathererAffectedArea.Chunks);
-            }
-        }
-
-        foreach (var (res, chunks) in chunksToRerender)
-        {
-            affectedAndNonRerenderedChunks[res].ExceptWith(chunks);
-            nonRerenderedChunksAffectedBeforeLastRerenderDelayed[res].ExceptWith(chunks);
-        }
-    }
-
-    private void Render(AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
-    {
-        Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender =
-            FindGlobalChunksToRerender(chunkGatherer, rerenderDelayed);
-
-        ChunkResolution onionSkinResolution = chunksToRerender.Min(x => x.Key);
-
-        bool updatingStoredChunks = false;
-        foreach (var (res, stored) in affectedAndNonRerenderedChunks)
-        {
-            HashSet<VecI> storedCopy = new HashSet<VecI>(stored);
-            storedCopy.IntersectWith(chunksToRerender[res]);
-            if (storedCopy.Count > 0)
-            {
-                updatingStoredChunks = true;
-                break;
-            }
-        }
-
-        UpdateAffectedNonRerenderedChunks(chunksToRerender, chunkGatherer.MainImageArea);
-
-        bool anythingToUpdate = false;
-        foreach (var (_, chunks) in chunksToRerender)
-        {
-            anythingToUpdate |= chunks.Count > 0;
-        }
-
-        if (!anythingToUpdate)
-            return;
-
-        UpdateMainImage(chunksToRerender, updatingStoredChunks ? null : chunkGatherer.MainImageArea.GlobalArea.Value);
-    }
-
-    private void UpdateMainImage(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender,
-        RectI? globalClippingRectangle)
-    {
-        if (chunksToRerender.Count == 0)
-            return;
-
-        foreach (var (resolution, chunks) in chunksToRerender)
-        {
-            int chunkSize = resolution.PixelSize();
-            RectI? globalScaledClippingRectangle = null;
-            if (globalClippingRectangle is not null)
-                globalScaledClippingRectangle =
-                    (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
-
-            foreach (var chunkPos in chunks)
-            {
-                RenderChunk(chunkPos, resolution);
-                RectI chunkRect = new(chunkPos * chunkSize, new(chunkSize, chunkSize));
-                if (globalScaledClippingRectangle is RectI rect)
-                    chunkRect = chunkRect.Intersect(rect);
-            }
-        }
-    }
-
-    private void RenderChunk(VecI chunkPos, ChunkResolution resolution)
-    {
-        doc.Renderer.UpdateChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime);
-    }
-}

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

@@ -1,15 +1,16 @@
 #nullable enable
 
+using Drawie.Backend.Core;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using Drawie.Backend.Core.Surfaces.ImageData;
-using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.Models.DocumentPassthroughActions;
+using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Models.Rendering;
 
@@ -20,6 +21,9 @@ internal class MemberPreviewUpdater
 
     private AnimationKeyFramePreviewRenderer AnimationKeyFramePreviewRenderer { get; }
 
+    private HashSet<Guid> queuedMembersToUpdate = new();
+    private HashSet<Guid> queuedMasksToUpdate = new();
+
     public MemberPreviewUpdater(IDocument doc, DocumentInternalParts internals)
     {
         this.doc = doc;
@@ -27,16 +31,17 @@ internal class MemberPreviewUpdater
         AnimationKeyFramePreviewRenderer = new AnimationKeyFramePreviewRenderer(internals);
     }
 
-    public void UpdatePreviews(HashSet<Guid> membersToUpdate,
+    public Dictionary<Guid, List<PreviewRenderRequest>> GatherPreviewsToUpdate(HashSet<Guid> membersToUpdate,
         HashSet<Guid> masksToUpdate, HashSet<Guid> nodesToUpdate, HashSet<Guid> keyFramesToUpdate,
-        bool ignoreAnimationPreviews, bool renderMiniPreviews)
+        bool ignoreAnimationPreviews, bool renderLowPriorityPreviews)
     {
         if (!membersToUpdate.Any() && !masksToUpdate.Any() && !nodesToUpdate.Any() &&
-            !keyFramesToUpdate.Any())
-            return;
+            !keyFramesToUpdate.Any() && !queuedMembersToUpdate.Any() && !queuedMasksToUpdate.Any())
+            return null;
 
-        UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate, ignoreAnimationPreviews,
-            renderMiniPreviews);
+        return UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate,
+            ignoreAnimationPreviews,
+            renderLowPriorityPreviews);
     }
 
     /// <summary>
@@ -44,35 +49,66 @@ internal class MemberPreviewUpdater
     /// </summary>
     /// <param name="members">Members that should be rendered</param>
     /// <param name="masksToUpdate">Masks that should be rendered</param>
-    private void UpdatePreviewPainters(HashSet<Guid> members, HashSet<Guid> masksToUpdate,
+    private Dictionary<Guid, List<PreviewRenderRequest>>? UpdatePreviewPainters(HashSet<Guid> members,
+        HashSet<Guid> masksToUpdate,
         HashSet<Guid> nodesToUpdate, HashSet<Guid> keyFramesToUpdate, bool ignoreAnimationPreviews,
         bool renderLowPriorityPreviews)
     {
-        RenderWholeCanvasPreview(renderLowPriorityPreviews);
+        Dictionary<Guid, List<PreviewRenderRequest>> previewTextures = new();
         if (renderLowPriorityPreviews)
         {
-            RenderLayersPreview(members);
-            RenderMaskPreviews(masksToUpdate);
+            members.UnionWith(queuedMembersToUpdate);
+            masksToUpdate.UnionWith(queuedMasksToUpdate);
+            RenderLayersPreview(members, previewTextures);
+            RenderMaskPreviews(masksToUpdate, previewTextures);
+
+            queuedMembersToUpdate.Clear();
+            queuedMasksToUpdate.Clear();
+        }
+        else
+        {
+            QueueMembersToUpdate(members);
+            QueueMasksToUpdate(masksToUpdate);
         }
 
         if (!ignoreAnimationPreviews)
         {
-            RenderAnimationPreviews(members, keyFramesToUpdate);
+            RenderAnimationPreviews(members, keyFramesToUpdate, previewTextures);
         }
 
-        RenderNodePreviews(nodesToUpdate);
+        RenderNodePreviews(nodesToUpdate, previewTextures);
+
+        return previewTextures;
+    }
+
+    private void QueueMembersToUpdate(HashSet<Guid> members)
+    {
+        foreach (var member in members)
+        {
+            queuedMembersToUpdate.Add(member);
+        }
+    }
+
+    private void QueueMasksToUpdate(HashSet<Guid> masks)
+    {
+        foreach (var mask in masks)
+        {
+            queuedMasksToUpdate.Add(mask);
+        }
     }
 
     /// <summary>
     /// Re-renders the preview of the whole canvas which is shown as the tab icon
     /// </summary>
+    /// <param name="memberGuids"></param>
+    /// <param name="previewTextures"></param>
     /// <param name="renderMiniPreviews">Decides whether to re-render mini previews for the document</param>
-    private void RenderWholeCanvasPreview(bool renderMiniPreviews)
+    /*private void RenderWholeCanvasPreview(bool renderMiniPreviews)
     {
         var previewSize = StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size);
         //float scaling = (float)previewSize.X / doc.SizeBindable.X;
 
-        doc.PreviewPainter ??= new PreviewPainter(doc.Renderer, doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
+        doc.PreviewPainter ??= new PreviewPainter(renderer, doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
             doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
 
         UpdateDocPreviewPainter(doc.PreviewPainter);
@@ -80,7 +116,7 @@ internal class MemberPreviewUpdater
         if (!renderMiniPreviews)
             return;
 
-        doc.MiniPreviewPainter ??= new PreviewPainter(doc.Renderer, doc.Renderer,
+        doc.MiniPreviewPainter ??= new PreviewPainter(renderer, doc.Renderer,
             doc.AnimationHandler.ActiveFrameTime,
             doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
 
@@ -93,9 +129,10 @@ internal class MemberPreviewUpdater
         painter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
         painter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
         painter.Repaint();
-    }
+    }*/
 
-    private void RenderLayersPreview(HashSet<Guid> memberGuids)
+    private void RenderLayersPreview(HashSet<Guid> memberGuids,
+        Dictionary<Guid, List<PreviewRenderRequest>> previewTextures)
     {
         foreach (var node in doc.NodeGraphHandler.AllNodes)
         {
@@ -104,32 +141,50 @@ internal class MemberPreviewUpdater
                 if (!memberGuids.Contains(node.Id))
                     continue;
 
-                if (structureMemberHandler.PreviewPainter == null)
+                var member = internals.Tracker.Document.FindMember(node.Id);
+                if (structureMemberHandler.Preview == null)
+                {
+                    structureMemberHandler.Preview = new TexturePreview(node.Id, RequestRender);
+                    continue;
+                }
+
+                if (structureMemberHandler.Preview.Listeners.Count == 0)
                 {
-                    var member = internals.Tracker.Document.FindMember(node.Id);
-                    if (member is not IPreviewRenderable previewRenderable)
-                        continue;
-
-                    structureMemberHandler.PreviewPainter =
-                        new PreviewPainter(doc.Renderer, previewRenderable,
-                            doc.AnimationHandler.ActiveFrameTime, doc.SizeBindable,
-                            internals.Tracker.Document.ProcessingColorSpace);
-                    structureMemberHandler.PreviewPainter.Repaint();
+                    structureMemberHandler.Preview.Preview?.Dispose();
+                    continue;
                 }
-                else
+
+                if (!previewTextures.ContainsKey(node.Id))
+                    previewTextures[node.Id] = new List<PreviewRenderRequest>();
+
+                VecI textureSize = structureMemberHandler.Preview.GetMaxListenerSize();
+                if (textureSize.X <= 0 || textureSize.Y <= 0)
+                    continue;
+
+                Texture? CreateTextureForLayer(bool createIfNull)
                 {
-                    structureMemberHandler.PreviewPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
-                    structureMemberHandler.PreviewPainter.DocumentSize = doc.SizeBindable;
-                    structureMemberHandler.PreviewPainter.ProcessingColorSpace =
-                        internals.Tracker.Document.ProcessingColorSpace;
+                    if (createIfNull)
+                    {
+                        if (structureMemberHandler.Preview.Preview == null ||
+                            structureMemberHandler.Preview.Preview.IsDisposed ||
+                            structureMemberHandler.Preview.Preview.Size != textureSize)
+                        {
+                            structureMemberHandler.Preview.Preview?.Dispose();
+                            structureMemberHandler.Preview.Preview = Texture.ForDisplay(textureSize);
+                        }
+                    }
 
-                    structureMemberHandler.PreviewPainter.Repaint();
+                    return structureMemberHandler.Preview.Preview;
                 }
+
+                previewTextures[node.Id].Add(new PreviewRenderRequest(CreateTextureForLayer,
+                    structureMemberHandler.Preview.InvokeTextureUpdated));
             }
         }
     }
 
-    private void RenderAnimationPreviews(HashSet<Guid> memberGuids, HashSet<Guid> keyFramesGuids)
+    private void RenderAnimationPreviews(HashSet<Guid> memberGuids, HashSet<Guid> keyFramesGuids,
+        Dictionary<Guid, List<PreviewRenderRequest>> previewTextures)
     {
         foreach (var keyFrame in doc.AnimationHandler.KeyFrames)
         {
@@ -144,13 +199,13 @@ internal class MemberPreviewUpdater
                             continue;
                     }
 
-                    RenderFramePreview(childFrame);
+                    RenderFramePreview(childFrame, previewTextures);
                 }
 
-                if (!memberGuids.Contains(groupHandler.LayerGuid))
+                if (!keyFramesGuids.Contains(groupHandler.LayerGuid) && !memberGuids.Contains(groupHandler.LayerGuid))
                     continue;
 
-                RenderGroupPreview(groupHandler);
+                RenderGroupPreview(groupHandler, previewTextures);
             }
         }
     }
@@ -161,56 +216,97 @@ internal class MemberPreviewUpdater
                cel.StartFrameBindable + cel.DurationBindable > doc.AnimationHandler.ActiveFrameBindable;
     }
 
-    private void RenderFramePreview(ICelHandler cel)
+    private void RenderFramePreview(ICelHandler cel, Dictionary<Guid, List<PreviewRenderRequest>> previewTextures)
     {
         if (internals.Tracker.Document.AnimationData.TryFindKeyFrame(cel.Id, out KeyFrame _))
         {
-            KeyFrameTime frameTime = doc.AnimationHandler.ActiveFrameTime;
-            if (cel.PreviewPainter == null)
+            if (cel.PreviewTexture == null)
             {
-                cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime,
-                    doc.SizeBindable,
-                    internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
+                cel.PreviewTexture = new TexturePreview(cel.LayerGuid, cel.Id, RequestCelRender);
+                return;
             }
-            else
+
+            if (cel.PreviewTexture.Listeners.Count == 0)
             {
-                cel.PreviewPainter.FrameTime = frameTime;
-                cel.PreviewPainter.DocumentSize = doc.SizeBindable;
-                cel.PreviewPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
+                cel.PreviewTexture.Preview?.Dispose();
+                return;
             }
 
-            cel.PreviewPainter.Repaint();
+            VecI textureSize = cel.PreviewTexture.GetMaxListenerSize();
+            if (textureSize.X <= 0 || textureSize.Y <= 0)
+                return;
+
+            Texture? CreateTextureForCel(bool createIfNull)
+            {
+                if (createIfNull)
+                {
+                    if (cel.PreviewTexture.Preview == null || cel.PreviewTexture.Preview.IsDisposed ||
+                        cel.PreviewTexture.Preview.Size != textureSize)
+                    {
+                        cel.PreviewTexture.Preview?.Dispose();
+                        cel.PreviewTexture.Preview = Texture.ForDisplay(textureSize);
+                    }
+                }
+
+                return cel.PreviewTexture.Preview;
+            }
+
+            if (!previewTextures.ContainsKey(cel.LayerGuid))
+                previewTextures[cel.LayerGuid] = new List<PreviewRenderRequest>();
+
+            previewTextures[cel.LayerGuid].Add(new PreviewRenderRequest(CreateTextureForCel,
+                cel.PreviewTexture.InvokeTextureUpdated, cel.Id.ToString()));
         }
     }
 
-    private void RenderGroupPreview(ICelGroupHandler groupHandler)
+    private void RenderGroupPreview(ICelGroupHandler groupHandler,
+        Dictionary<Guid, List<PreviewRenderRequest>> previewTextures)
     {
         var group = internals.Tracker.Document.AnimationData.KeyFrames.FirstOrDefault(x => x.Id == groupHandler.Id);
         if (group != null)
         {
-            KeyFrameTime frameTime = doc.AnimationHandler.ActiveFrameTime;
-            ColorSpace processingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
-            VecI documentSize = doc.SizeBindable;
+            if (groupHandler.PreviewTexture == null)
+            {
+                groupHandler.PreviewTexture = new TexturePreview(groupHandler.LayerGuid, groupHandler.LayerGuid, RequestCelRender);
+                return;
+            }
 
-            if (groupHandler.PreviewPainter == null)
+            if (groupHandler.PreviewTexture.Listeners.Count == 0)
             {
-                groupHandler.PreviewPainter =
-                    new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize,
-                        processingColorSpace,
-                        groupHandler.Id.ToString());
+                groupHandler.PreviewTexture.Preview?.Dispose();
+                return;
             }
-            else
+
+            VecI textureSize = groupHandler.PreviewTexture.GetMaxListenerSize();
+            if (textureSize.X <= 0 || textureSize.Y <= 0)
+                return;
+
+            Texture? CreateTextureForGroup(bool createIfNull)
             {
-                groupHandler.PreviewPainter.FrameTime = frameTime;
-                groupHandler.PreviewPainter.DocumentSize = documentSize;
-                groupHandler.PreviewPainter.ProcessingColorSpace = processingColorSpace;
+                if (createIfNull)
+                {
+                    if (groupHandler.PreviewTexture.Preview == null ||
+                        groupHandler.PreviewTexture.Preview.IsDisposed ||
+                        groupHandler.PreviewTexture.Preview.Size != textureSize)
+                    {
+                        groupHandler.PreviewTexture.Preview?.Dispose();
+                        groupHandler.PreviewTexture.Preview = Texture.ForDisplay(textureSize);
+                    }
+                }
+
+                return groupHandler.PreviewTexture.Preview;
             }
 
-            groupHandler.PreviewPainter.Repaint();
+            if (!previewTextures.ContainsKey(groupHandler.LayerGuid))
+                previewTextures[groupHandler.LayerGuid] = new List<PreviewRenderRequest>();
+
+            previewTextures[groupHandler.LayerGuid].Add(new PreviewRenderRequest(CreateTextureForGroup,
+                groupHandler.PreviewTexture.InvokeTextureUpdated, groupHandler.Id.ToString()));
         }
     }
 
-    private void RenderMaskPreviews(HashSet<Guid> members)
+    private void RenderMaskPreviews(HashSet<Guid> members,
+        Dictionary<Guid, List<PreviewRenderRequest>> previewTextures)
     {
         foreach (var node in doc.NodeGraphHandler.AllNodes)
         {
@@ -219,34 +315,49 @@ internal class MemberPreviewUpdater
                 if (!members.Contains(node.Id))
                     continue;
 
-                var member = internals.Tracker.Document.FindMember(node.Id);
-                if (member is not IPreviewRenderable previewRenderable)
+                if (structureMemberHandler.MaskPreview == null)
+                {
+                    structureMemberHandler.MaskPreview = new TexturePreview(node.Id, RequestMaskRender);
                     continue;
+                }
 
-                if (structureMemberHandler.MaskPreviewPainter == null)
+                if (structureMemberHandler.MaskPreview.Listeners.Count == 0)
                 {
-                    structureMemberHandler.MaskPreviewPainter = new PreviewPainter(
-                        doc.Renderer,
-                        previewRenderable,
-                        doc.AnimationHandler.ActiveFrameTime,
-                        doc.SizeBindable,
-                        internals.Tracker.Document.ProcessingColorSpace,
-                        nameof(StructureNode.EmbeddedMask));
+                    structureMemberHandler.MaskPreview.Preview?.Dispose();
+                    continue;
                 }
-                else
+
+                if (!previewTextures.ContainsKey(node.Id))
+                    previewTextures[node.Id] = new List<PreviewRenderRequest>();
+
+                VecI textureSize = structureMemberHandler.MaskPreview.GetMaxListenerSize();
+                if (textureSize.X <= 0 || textureSize.Y <= 0)
+                    continue;
+
+                Texture? CreateTextureForMask(bool createIfNull)
                 {
-                    structureMemberHandler.MaskPreviewPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
-                    structureMemberHandler.MaskPreviewPainter.DocumentSize = doc.SizeBindable;
-                    structureMemberHandler.MaskPreviewPainter.ProcessingColorSpace =
-                        internals.Tracker.Document.ProcessingColorSpace;
+                    if (createIfNull)
+                    {
+                        if (structureMemberHandler.MaskPreview.Preview == null ||
+                            structureMemberHandler.MaskPreview.Preview.IsDisposed ||
+                            structureMemberHandler.MaskPreview.Preview.Size != textureSize)
+                        {
+                            structureMemberHandler.MaskPreview.Preview?.Dispose();
+                            structureMemberHandler.MaskPreview.Preview = Texture.ForDisplay(textureSize);
+                        }
+                    }
+
+                    return structureMemberHandler.MaskPreview.Preview;
                 }
 
-                structureMemberHandler.MaskPreviewPainter.Repaint();
+                previewTextures[node.Id].Add(new PreviewRenderRequest(CreateTextureForMask,
+                    structureMemberHandler.MaskPreview.InvokeTextureUpdated) {ElementToRender = nameof(StructureNode.EmbeddedMask) });
             }
         }
     }
 
-    private void RenderNodePreviews(HashSet<Guid> nodesGuids)
+    private void RenderNodePreviews(HashSet<Guid> nodesGuids,
+        Dictionary<Guid, List<PreviewRenderRequest>>? previews = null)
     {
         var outputNode = internals.Tracker.Document.NodeGraph.OutputNode;
 
@@ -263,12 +374,33 @@ internal class MemberPreviewUpdater
         List<Guid> actualRepaintedNodes = new();
         foreach (var guid in nodesGuids)
         {
-            QueueRepaintNode(actualRepaintedNodes, guid, allNodes);
+            QueueRepaintNode(actualRepaintedNodes, guid, allNodes, previews);
         }
     }
 
+    private bool IsSmallStructureNode(Guid guid, Dictionary<Guid, List<PreviewRenderRequest>>? previews)
+    {
+        var node = doc.StructureHelper.FindNode<INodeHandler>(guid);
+        if (node is null)
+            return false;
+
+        if (node is not IStructureMemberHandler memberHandler)
+            return false;
+
+        var member = internals.Tracker.Document.FindMember(guid);
+        if (member is null)
+            return false;
+
+        if(previews == null || !previews.ContainsKey(member.Id))
+            return false;
+
+        var value = previews[guid];
+
+        return value is { Count: > 0 } && value.Max(x => x.Texture.Size.LongestAxis < 64);
+    }
+
     private void QueueRepaintNode(List<Guid> actualRepaintedNodes, Guid guid,
-        IReadOnlyCollection<IReadOnlyNode> allNodes)
+        IReadOnlyCollection<IReadOnlyNode> allNodes, Dictionary<Guid, List<PreviewRenderRequest>>? previews)
     {
         if (actualRepaintedNodes.Contains(guid))
             return;
@@ -284,7 +416,7 @@ internal class MemberPreviewUpdater
         if (node is null)
             return;
 
-        RequestRepaintNode(node, nodeVm);
+        RequestRepaintNode(node, nodeVm, previews);
 
         nodeVm.TraverseForwards(next =>
         {
@@ -296,32 +428,69 @@ internal class MemberPreviewUpdater
             if (nextNode is null || actualRepaintedNodes.Contains(next.Id))
                 return Traverse.Further;
 
-            RequestRepaintNode(nextNode, nextVm);
+            RequestRepaintNode(nextNode, nextVm, previews);
             actualRepaintedNodes.Add(next.Id);
             return Traverse.Further;
         });
     }
 
-    private void RequestRepaintNode(IReadOnlyNode node, INodeHandler nodeVm)
+    private void RequestRepaintNode(IReadOnlyNode node, INodeHandler nodeVm,
+        Dictionary<Guid, List<PreviewRenderRequest>>? previews)
     {
-        if (node is IPreviewRenderable renderable)
+        if (previews == null)
+            return;
+
+        nodeVm.Preview ??= new TexturePreview(node.Id, RequestRender);
+        if (nodeVm.Preview.Listeners.Count == 0)
         {
-            if (nodeVm.ResultPainter == null)
+            nodeVm.Preview.Preview?.Dispose();
+            return;
+        }
+
+        if (!previews.ContainsKey(node.Id))
+            previews[node.Id] = new List<PreviewRenderRequest>();
+
+        if (previews.TryGetValue(node.Id, out var existingPreviews) &&
+            existingPreviews.Any(x => string.IsNullOrEmpty(x.ElementToRender)))
+            return;
+
+        VecI textureSize = nodeVm.Preview.GetMaxListenerSize();
+        if (nodeVm is IStructureMemberHandler && textureSize.LongestAxis < 64)
+            return;
+        if (textureSize.X <= 0 || textureSize.Y <= 0)
+            return;
+
+        Texture? CreateTextureForNode(bool createIfNull)
+        {
+            if (createIfNull)
             {
-                nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable,
-                    doc.AnimationHandler.ActiveFrameTime,
-                    doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
-                nodeVm.ResultPainter.AllowPartialResolutions = false;
-                nodeVm.ResultPainter.Repaint();
+                if (nodeVm.Preview.Preview == null || nodeVm.Preview.Preview.IsDisposed ||
+                    nodeVm.Preview.Preview.Size != textureSize)
+                {
+                    nodeVm.Preview.Preview?.Dispose();
+                    nodeVm.Preview.Preview = Texture.ForDisplay(textureSize);
+                }
             }
-            else
-            {
-                nodeVm.ResultPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
-                nodeVm.ResultPainter.DocumentSize = doc.SizeBindable;
-                nodeVm.ResultPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
 
-                nodeVm.ResultPainter?.Repaint();
-            }
-        }
+            return nodeVm.Preview.Preview;
+        };
+
+        previews[node.Id]
+            .Add(new PreviewRenderRequest(CreateTextureForNode, nodeVm.Preview.InvokeTextureUpdated));
+    }
+
+    private void RequestRender(Guid id)
+    {
+        internals.ActionAccumulator.AddActions(new RefreshPreview_PassthroughAction(id));
+    }
+
+    private void RequestCelRender(Guid nodeId, Guid? celId)
+    {
+        internals.ActionAccumulator.AddActions(new RefreshPreview_PassthroughAction(nodeId, celId));
+    }
+
+    private void RequestMaskRender(Guid id)
+    {
+        internals.ActionAccumulator.AddActions(new RefreshPreview_PassthroughAction(id, null, nameof(StructureNode.EmbeddedMask)));
     }
 }

+ 0 - 284
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -1,284 +0,0 @@
-using Avalonia;
-using Avalonia.Threading;
-using ChunkyImageLib.DataHolders;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.ColorsImpl;
-using Drawie.Backend.Core.Numerics;
-using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using Drawie.Backend.Core.Surfaces;
-using Drawie.Backend.Core.Surfaces.ImageData;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Numerics;
-using PixiEditor.ChangeableDocument.Rendering;
-
-namespace PixiEditor.Models.Rendering;
-
-public class PreviewPainter : IDisposable
-{
-    public string ElementToRenderName { get; set; }
-    public IPreviewRenderable PreviewRenderable { get; set; }
-    public ColorSpace ProcessingColorSpace { get; set; }
-    public KeyFrameTime FrameTime { get; set; }
-    public VecI DocumentSize { get; set; }
-    public DocumentRenderer Renderer { get; set; }
-
-    public bool AllowPartialResolutions { get; set; } = true;
-
-    public bool CanRender => canRender;
-
-    public event Action<bool>? CanRenderChanged;
-
-    private Dictionary<int, Texture> renderTextures = new();
-    private Dictionary<int, PainterInstance> painterInstances = new();
-
-    private HashSet<int> dirtyTextures = new HashSet<int>();
-    private HashSet<int> repaintingTextures = new HashSet<int>();
-
-    private Dictionary<int, VecI> pendingResizes = new();
-    private HashSet<int> pendingRemovals = new();
-
-    private bool canRender;
-
-    private int lastRequestId = 0;
-
-    public PreviewPainter(DocumentRenderer renderer, IPreviewRenderable previewRenderable, KeyFrameTime frameTime,
-        VecI documentSize, ColorSpace processingColorSpace, string elementToRenderName = "")
-    {
-        PreviewRenderable = previewRenderable;
-        ElementToRenderName = elementToRenderName;
-        ProcessingColorSpace = processingColorSpace;
-        FrameTime = frameTime;
-        DocumentSize = documentSize;
-        Renderer = renderer;
-    }
-
-    public void Paint(DrawingSurface renderOn, int painterId)
-    {
-        if (!renderTextures.TryGetValue(painterId, out Texture? renderTexture))
-        {
-            return;
-        }
-
-        if (renderTexture == null || renderTexture.IsDisposed)
-        {
-            return;
-        }
-
-        renderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
-    }
-
-    public PainterInstance AttachPainterInstance()
-    {
-        int requestId = lastRequestId++;
-
-        PainterInstance painterInstance = new() { RequestId = requestId };
-
-        painterInstances[requestId] = painterInstance;
-
-        return painterInstance;
-    }
-
-    public void ChangeRenderTextureSize(int requestId, VecI size)
-    {
-        if (size.X <= 0 || size.Y <= 0)
-        {
-            return;
-        }
-
-        if (repaintingTextures.Contains(requestId))
-        {
-            pendingResizes[requestId] = size;
-            return;
-        }
-
-        if (renderTextures.ContainsKey(requestId))
-        {
-            renderTextures[requestId].Dispose();
-        }
-
-        renderTextures[requestId] = Texture.ForProcessing(size, ProcessingColorSpace);
-    }
-
-    public void RemovePainterInstance(int requestId)
-    {
-        painterInstances.Remove(requestId);
-        dirtyTextures.Remove(requestId);
-
-        if (repaintingTextures.Contains(requestId))
-        {
-            pendingRemovals.Add(requestId);
-            return;
-        }
-
-        if (renderTextures.TryGetValue(requestId, out var renderTexture))
-        {
-            renderTexture?.Dispose();
-            renderTextures.Remove(requestId);
-        }
-    }
-
-    public void Repaint()
-    {
-        foreach (var texture in renderTextures)
-        {
-            dirtyTextures.Add(texture.Key);
-        }
-
-        RepaintDirty();
-    }
-
-    public void RepaintFor(int requestId)
-    {
-        dirtyTextures.Add(requestId);
-        RepaintDirty();
-    }
-
-    private void RepaintDirty()
-    {
-        var dirtyArray = dirtyTextures.ToArray();
-        bool couldRender = canRender;
-        canRender = PreviewRenderable?.GetPreviewBounds(FrameTime.Frame, ElementToRenderName) != null &&
-                    painterInstances.Count > 0;
-        if (couldRender != canRender)
-        {
-            CanRenderChanged?.Invoke(canRender);
-        }
-
-        if (!CanRender)
-        {
-            return;
-        }
-
-        foreach (var texture in dirtyArray)
-        {
-            if (!renderTextures.TryGetValue(texture, out var renderTexture))
-            {
-                continue;
-            }
-
-            if (!painterInstances.TryGetValue(texture, out var painterInstance))
-            {
-                repaintingTextures.Remove(texture);
-                dirtyTextures.Remove(texture);
-                continue;
-            }
-
-            repaintingTextures.Add(texture);
-
-            renderTexture.DrawingSurface.Canvas.Clear();
-            renderTexture.DrawingSurface.Canvas.Save();
-
-            Matrix3X3? matrix = painterInstance.RequestMatrix?.Invoke();
-            VecI bounds = painterInstance.RequestRenderBounds?.Invoke() ?? VecI.Zero;
-
-            ChunkResolution finalResolution = FindResolution(bounds);
-            SamplingOptions samplingOptions = FindSamplingOptions(matrix);
-
-            renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
-            renderTexture.DrawingSurface.Canvas.Scale((float)finalResolution.InvertedMultiplier());
-
-            RenderContext context = new(renderTexture.DrawingSurface, FrameTime, finalResolution,
-                DocumentSize,
-                DocumentSize,
-                ProcessingColorSpace, samplingOptions);
-
-            dirtyTextures.Remove(texture);
-            Renderer.RenderNodePreview(PreviewRenderable, renderTexture.DrawingSurface, context, ElementToRenderName)
-                .ContinueWith(_ =>
-                {
-                    Dispatcher.UIThread.Invoke(() =>
-                    {
-                        if (pendingRemovals.Contains(texture))
-                        {
-                            if (!renderTexture.IsDisposed)
-                            {
-                                try
-                                {
-                                    renderTexture.Dispose();
-                                }
-                                catch (Exception) { }
-                            }
-
-                            renderTextures.Remove(texture);
-                            pendingRemovals.Remove(texture);
-                            pendingResizes.Remove(texture);
-                            dirtyTextures.Remove(texture);
-                            return;
-                        }
-
-                        if (renderTexture is { IsDisposed: false })
-                        {
-                            try
-                            {
-                                renderTexture.DrawingSurface.Canvas.Restore();
-                            }
-                            catch (Exception)
-                            {
-                                repaintingTextures.Remove(texture);
-                                dirtyTextures.Remove(texture);
-                                pendingResizes.Remove(texture);
-                                return;
-                            }
-                        }
-
-                        painterInstance.RequestRepaint?.Invoke();
-                        repaintingTextures.Remove(texture);
-
-                        if (pendingResizes.Remove(texture, out VecI size))
-                        {
-                            ChangeRenderTextureSize(texture, size);
-                            dirtyTextures.Add(texture);
-                        }
-
-                        if (repaintingTextures.Count == 0 && dirtyTextures.Count > 0)
-                        {
-                            RepaintDirty();
-                        }
-                    });
-                });
-        }
-    }
-
-    private ChunkResolution FindResolution(VecI bounds)
-    {
-        if (bounds.X <= 0 || bounds.Y <= 0 || !AllowPartialResolutions)
-        {
-            return ChunkResolution.Full;
-        }
-
-        double density = DocumentSize.X / (double)bounds.X;
-        if (density > 8.01)
-            return ChunkResolution.Eighth;
-        if (density > 4.01)
-            return ChunkResolution.Quarter;
-        if (density > 2.01)
-            return ChunkResolution.Half;
-        return ChunkResolution.Full;
-    }
-
-    private SamplingOptions FindSamplingOptions(Matrix3X3? matrix)
-    {
-        Matrix3X3 mtx = matrix ?? Matrix3X3.Identity;
-        return mtx.ScaleX < 1f || mtx.ScaleY < 1f
-            ? SamplingOptions.Bilinear
-            : SamplingOptions.Default;
-    }
-
-    public void Dispose()
-    {
-        foreach (var texture in renderTextures)
-        {
-            texture.Value.Dispose();
-        }
-    }
-}
-
-public class PainterInstance
-{
-    public int RequestId { get; set; }
-    public Func<VecI> RequestRenderBounds;
-
-    public Func<Matrix3X3?>? RequestMatrix;
-    public Action RequestRepaint;
-}

+ 240 - 148
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -1,33 +1,38 @@
-using ChunkyImageLib.DataHolders;
+using Avalonia.Threading;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Position;
 
 namespace PixiEditor.Models.Rendering;
 
-internal class SceneRenderer : IDisposable
+internal class SceneRenderer
 {
     public const double ZoomDiffToRerender = 20;
+    public const float OversizeFactor = 1.25f;
     public IReadOnlyDocument Document { get; }
     public IDocument DocumentViewModel { get; }
     public bool HighResRendering { get; set; } = true;
 
-    private Dictionary<string, Texture> cachedTextures = new();
-    private bool lastHighResRendering = true;
+    public IReadOnlyDictionary<Guid, RenderState> LastRenderedStates => lastRenderedStates;
+    private Dictionary<Guid, RenderState> lastRenderedStates = new();
     private int lastGraphCacheHash = -1;
     private KeyFrameTime lastFrameTime;
     private Dictionary<Guid, bool> lastFramesVisibility = new();
 
-    private ChunkResolution? lastResolution;
+    private TextureCache textureCache = new();
 
     public SceneRenderer(IReadOnlyDocument trackerDocument, IDocument documentViewModel)
     {
@@ -35,110 +40,220 @@ internal class SceneRenderer : IDisposable
         DocumentViewModel = documentViewModel;
     }
 
-    public void RenderScene(DrawingSurface target, ChunkResolution resolution, SamplingOptions samplingOptions,
-        string? targetOutput = null)
+    public async Task RenderAsync(Dictionary<Guid, ViewportInfo> stateViewports, AffectedArea affectedArea,
+        bool updateDelayed, Dictionary<Guid, List<PreviewRenderRequest>>? previewTextures, bool immediateRender)
     {
-        if (Document.Renderer.IsBusy || DocumentViewModel.Busy ||
-            target.DeviceClipBounds.Size.ShortestAxis <= 0) return;
-        RenderOnionSkin(target, resolution, samplingOptions, targetOutput);
+        if (immediateRender)
+        {
+            Render(stateViewports, affectedArea, updateDelayed, previewTextures);
+            return;
+        }
 
-        string adjustedTargetOutput = targetOutput ?? "";
+        await DrawingBackendApi.Current.RenderingDispatcher.InvokeInBackgroundAsync(() =>
+        {
+            Render(stateViewports, affectedArea, updateDelayed, previewTextures);
+        });
+    }
 
-        IReadOnlyNodeGraph finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
-        bool shouldRerender = ShouldRerender(target, resolution, adjustedTargetOutput, finalGraph);
+    private void Render(Dictionary<Guid, ViewportInfo> stateViewports, AffectedArea affectedArea, bool updateDelayed,
+        Dictionary<Guid, List<PreviewRenderRequest>>? previewTextures)
+    {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+        int renderedCount = 0;
+        foreach (var viewport in stateViewports)
+        {
+            if (viewport.Value.Delayed && !updateDelayed)
+            {
+                continue;
+            }
 
-        // TODO: Check if clipping to visible area improves performance on full resolution
-        // Meaning zoomed big textures
+            if (viewport.Value.RealDimensions.ShortestAxis <= 0) continue;
 
-        if (shouldRerender)
-        {
-            if (cachedTextures.ContainsKey(adjustedTargetOutput))
+            var rendered = RenderScene(viewport.Value, affectedArea, previewTextures);
+            if (DocumentViewModel.SceneTextures.TryGetValue(viewport.Key, out var texture) && texture != rendered)
             {
-                cachedTextures[adjustedTargetOutput]?.Dispose();
+                texture.Dispose();
             }
 
-            var rendered = RenderGraph(target, resolution, samplingOptions, targetOutput, finalGraph);
-            cachedTextures[adjustedTargetOutput] = rendered;
-            return;
+            DocumentViewModel.SceneTextures[viewport.Key] = rendered;
+            viewport.Value.InvalidateVisual();
+            renderedCount++;
         }
 
-        var cachedTexture = cachedTextures[adjustedTargetOutput];
-        Matrix3X3 matrixDiff = SolveMatrixDiff(target, cachedTexture);
-        int saved = target.Canvas.Save();
-        target.Canvas.SetMatrix(matrixDiff);
-        if (samplingOptions == SamplingOptions.Default)
+        if (renderedCount == 0 && previewTextures is { Count: > 0 })
         {
-            target.Canvas.DrawSurface(cachedTexture.DrawingSurface, 0, 0);
+            RenderOnlyPreviews(affectedArea, previewTextures);
         }
-        else
+    }
+
+    private void RenderOnlyPreviews(AffectedArea affectedArea,
+        Dictionary<Guid, List<PreviewRenderRequest>> previewTextures)
+    {
+        ViewportInfo previewGenerationViewport = new()
         {
-            using var img = cachedTexture.DrawingSurface.Snapshot();
-            target.Canvas.DrawImage(img, 0, 0, samplingOptions);
+            RealDimensions = new VecD(1, 1),
+            Transform = Matrix3X3.Identity,
+            Id = Guid.NewGuid(),
+            Resolution = ChunkResolution.Full,
+            Sampling = SamplingOptions.Bilinear,
+            VisibleDocumentRegion = null,
+            RenderOutput = "DEFAULT",
+            Delayed = false
+        };
+        var rendered = RenderScene(previewGenerationViewport, affectedArea, previewTextures);
+        rendered.Dispose();
+    }
+
+    public Texture? RenderScene(ViewportInfo viewport, AffectedArea affectedArea,
+        Dictionary<Guid, List<PreviewRenderRequest>>? previewTextures = null)
+    {
+        /*if (Document.Renderer.IsBusy || DocumentViewModel.Busy ||
+            target.DeviceClipBounds.Size.ShortestAxis <= 0) return;*/
+
+        /*TODO:
+         - [ ] Rendering optimizer
+         - [?] Render thread and proper locking/synchronization - check render-thread branch (both drawie and pixieditor)
+               but be aware, this is a nightmare and good luck
+         */
+
+        VecI renderTargetSize = (VecI)viewport.RealDimensions;
+        Matrix3X3 targetMatrix = viewport.Transform;
+        Guid viewportId = viewport.Id;
+        ChunkResolution resolution = viewport.Resolution;
+        SamplingOptions samplingOptions = viewport.Sampling;
+        RectI? visibleDocumentRegion = viewport.VisibleDocumentRegion;
+        string? targetOutput = viewport.RenderOutput.Equals("DEFAULT", StringComparison.InvariantCultureIgnoreCase)
+            ? null
+            : viewport.RenderOutput;
+
+        IReadOnlyNodeGraph finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
+
+        float oversizeFactor = 1;
+        if (visibleDocumentRegion != null && viewport.IsScene &&
+            visibleDocumentRegion.Value != new RectI(0, 0, Document.Size.X, Document.Size.Y))
+        {
+            visibleDocumentRegion = (RectI)visibleDocumentRegion.Value.Scale(OversizeFactor,
+                visibleDocumentRegion.Value.Center);
+            oversizeFactor = OversizeFactor;
         }
 
-        target.Canvas.RestoreToCount(saved);
+        bool shouldRerender =
+            ShouldRerender(renderTargetSize, targetMatrix, resolution, viewportId, targetOutput, finalGraph,
+                previewTextures, visibleDocumentRegion, oversizeFactor, out bool fullAffectedArea);
+
+        if (shouldRerender)
+        {
+            affectedArea = fullAffectedArea && viewport.VisibleDocumentRegion.HasValue
+                ? new AffectedArea(OperationHelper.FindChunksTouchingRectangle(viewport.VisibleDocumentRegion.Value,
+                    ChunkyImage.FullChunkSize))
+                : affectedArea;
+            return RenderGraph(renderTargetSize, targetMatrix, viewportId, resolution, samplingOptions, affectedArea,
+                visibleDocumentRegion, targetOutput, viewport.IsScene, oversizeFactor, finalGraph, previewTextures);
+        }
+
+        var cachedTexture = DocumentViewModel.SceneTextures[viewportId];
+        return cachedTexture;
     }
 
-    private Texture RenderGraph(DrawingSurface target, ChunkResolution resolution, SamplingOptions samplingOptions,
+    private Texture RenderGraph(VecI renderTargetSize, Matrix3X3 targetMatrix, Guid viewportId,
+        ChunkResolution resolution,
+        SamplingOptions samplingOptions,
+        AffectedArea area,
+        RectI? visibleDocumentRegion,
         string? targetOutput,
-        IReadOnlyNodeGraph finalGraph)
+        bool canRenderOnionSkinning,
+        float oversizeFactor,
+        IReadOnlyNodeGraph finalGraph, Dictionary<Guid, List<PreviewRenderRequest>>? previewTextures)
     {
-        DrawingSurface renderTarget = target;
+        DrawingSurface renderTarget = null;
         Texture? renderTexture = null;
         int restoreCanvasTo;
 
         VecI finalSize = SolveRenderOutputSize(targetOutput, finalGraph, Document.Size);
-        if (RenderInOutputSize(finalGraph))
+        if (RenderInOutputSize(finalGraph, renderTargetSize, finalSize))
         {
             finalSize = (VecI)(finalSize * resolution.Multiplier());
 
-            renderTexture = Texture.ForProcessing(finalSize, Document.ProcessingColorSpace);
+            renderTexture =
+                textureCache.RequestTexture(viewportId.GetHashCode(), finalSize, Document.ProcessingColorSpace);
             renderTarget = renderTexture.DrawingSurface;
+            renderTarget.Canvas.Save();
             renderTexture.DrawingSurface.Canvas.Save();
             renderTexture.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
-
-            restoreCanvasTo = target.Canvas.Save();
-            target.Canvas.Scale((float)resolution.InvertedMultiplier());
         }
         else
         {
-            renderTexture = Texture.ForProcessing(renderTarget.DeviceClipBounds.Size, Document.ProcessingColorSpace);
+            var bufferedSize = (VecI)(renderTargetSize * oversizeFactor);
+            renderTexture = textureCache.RequestTexture(viewportId.GetHashCode(), bufferedSize,
+                Document.ProcessingColorSpace);
+
+            var bufferedMatrix = targetMatrix.PostConcat(Matrix3X3.CreateTranslation(
+                (bufferedSize.X - renderTargetSize.X) / 2.0,
+                (bufferedSize.Y - renderTargetSize.Y) / 2.0));
 
             renderTarget = renderTexture.DrawingSurface;
+            renderTarget.Canvas.SetMatrix(bufferedMatrix);
+        }
 
-            restoreCanvasTo = target.Canvas.Save();
-            renderTarget.Canvas.Save();
+        bool renderOnionSkinning = canRenderOnionSkinning &&
+                                   DocumentViewModel.AnimationHandler.OnionSkinningEnabledBindable;
 
-            renderTarget.Canvas.SetMatrix(target.Canvas.TotalMatrix);
-            target.Canvas.SetMatrix(Matrix3X3.Identity);
-            renderTarget.Canvas.ClipRect(new RectD(0, 0, finalSize.X, finalSize.Y));
-            resolution = ChunkResolution.Full;
+        var animationData = Document.AnimationData;
+        double onionOpacity = animationData.OnionOpacity / 100.0;
+        double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
+        if (renderOnionSkinning)
+        {
+            for (int i = 1; i <= animationData.OnionFrames; i++)
+            {
+                int frame = DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame - i;
+                if (frame < DocumentViewModel.AnimationHandler.FirstVisibleFrame)
+                {
+                    break;
+                }
+
+                double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
+                RenderContext onionContext = new(renderTarget, frame, resolution, finalSize, Document.Size,
+                    Document.ProcessingColorSpace, samplingOptions, finalOpacity);
+                onionContext.TargetOutput = targetOutput;
+                onionContext.VisibleDocumentRegion = visibleDocumentRegion;
+                finalGraph.Execute(onionContext);
+            }
         }
 
         RenderContext context = new(renderTarget, DocumentViewModel.AnimationHandler.ActiveFrameTime,
             resolution, finalSize, Document.Size, Document.ProcessingColorSpace, samplingOptions);
         context.TargetOutput = targetOutput;
+        context.AffectedArea = area;
+        context.VisibleDocumentRegion = visibleDocumentRegion;
+        context.PreviewTextures = previewTextures;
         finalGraph.Execute(context);
 
-        if (renderTexture != null)
+        if (renderOnionSkinning)
         {
-            if (samplingOptions == SamplingOptions.Default)
-            {
-                target.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
-            }
-            else
+            for (int i = 1; i <= animationData.OnionFrames; i++)
             {
-                using var snapshot = renderTexture.DrawingSurface.Snapshot();
-                target.Canvas.DrawImage(snapshot, 0, 0, samplingOptions);
-            }
+                int frame = DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame + i;
+                if (frame >= DocumentViewModel.AnimationHandler.LastFrame)
+                {
+                    break;
+                }
 
-            target.Canvas.RestoreToCount(restoreCanvasTo);
+                double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
+                RenderContext onionContext = new(renderTarget, frame, resolution, finalSize, Document.Size,
+                    Document.ProcessingColorSpace, samplingOptions, finalOpacity);
+                onionContext.TargetOutput = targetOutput;
+                onionContext.VisibleDocumentRegion = visibleDocumentRegion;
+                finalGraph.Execute(onionContext);
+            }
         }
 
+        renderTarget.Canvas.Restore();
+
         return renderTexture;
     }
 
-    private static VecI SolveRenderOutputSize(string? targetOutput, IReadOnlyNodeGraph finalGraph, VecI documentSize)
+    private static VecI SolveRenderOutputSize(string? targetOutput, IReadOnlyNodeGraph finalGraph,
+        VecI documentSize)
     {
         VecI finalSize = documentSize;
         if (targetOutput != null)
@@ -162,38 +277,67 @@ internal class SceneRenderer : IDisposable
         return finalSize;
     }
 
-    private bool RenderInOutputSize(IReadOnlyNodeGraph finalGraph)
+    private bool RenderInOutputSize(IReadOnlyNodeGraph finalGraph, VecI renderTargetSize, VecI finalSize)
     {
-        return !HighResRendering || !HighDpiRenderNodePresent(finalGraph);
+        return !HighResRendering ||
+               (!HighDpiRenderNodePresent(finalGraph) && renderTargetSize.Length > finalSize.Length);
     }
 
-    private bool ShouldRerender(DrawingSurface target, ChunkResolution resolution, string? targetOutput,
-        IReadOnlyNodeGraph finalGraph)
+    private bool ShouldRerender(VecI targetSize, Matrix3X3 matrix, ChunkResolution resolution,
+        Guid viewportId,
+        string targetOutput,
+        IReadOnlyNodeGraph finalGraph, Dictionary<Guid, List<PreviewRenderRequest>>? previewTextures,
+        RectI? visibleDocumentRegion, float oversizeFactor, out bool fullAffectedArea)
     {
-        if (!cachedTextures.TryGetValue(targetOutput ?? "", out var cachedTexture) || cachedTexture == null ||
+        fullAffectedArea = false;
+        if (!DocumentViewModel.SceneTextures.TryGetValue(viewportId, out var cachedTexture) ||
+            cachedTexture == null ||
             cachedTexture.IsDisposed)
         {
             return true;
         }
 
-        if (lastResolution != resolution)
+        if (previewTextures is { Count: > 0 })
         {
-            lastResolution = resolution;
             return true;
         }
 
-        if (lastHighResRendering != HighResRendering)
+        var renderState = new RenderState
+        {
+            ChunkResolution = resolution,
+            HighResRendering = HighResRendering,
+            TargetOutput = targetOutput,
+            OnionFrames = Document.AnimationData.OnionFrames,
+            OnionOpacity = Document.AnimationData.OnionOpacity,
+            OnionSkinning = DocumentViewModel.AnimationHandler.OnionSkinningEnabledBindable,
+            GraphCacheHash = finalGraph.GetCacheHash(),
+            ZoomLevel = matrix.ScaleX,
+            VisibleDocumentRegion =
+                (RectD?)visibleDocumentRegion ?? new RectD(0, 0, Document.Size.X, Document.Size.Y)
+        };
+
+        if (lastRenderedStates.TryGetValue(viewportId, out var lastState))
         {
-            lastHighResRendering = HighResRendering;
+            if (lastState.ShouldRerender(renderState))
+            {
+                lastRenderedStates[viewportId] = renderState;
+                fullAffectedArea = lastState.ZoomLevel > renderState.ZoomLevel;
+                return true;
+            }
+        }
+        else
+        {
+            lastRenderedStates[viewportId] = renderState;
             return true;
         }
 
-        bool renderInDocumentSize = RenderInOutputSize(finalGraph);
+        VecI finalSize = SolveRenderOutputSize(targetOutput, finalGraph, Document.Size);
+        bool renderInDocumentSize = RenderInOutputSize(finalGraph, targetSize, finalSize);
         VecI compareSize = renderInDocumentSize
             ? (VecI)(Document.Size * resolution.Multiplier())
-            : target.DeviceClipBounds.Size;
+            : targetSize;
 
-        if (cachedTexture.DrawingSurface.DeviceClipBounds.Size != compareSize)
+        if (cachedTexture.Size != (VecI)(compareSize * oversizeFactor))
         {
             return true;
         }
@@ -221,37 +365,10 @@ internal class SceneRenderer : IDisposable
             }
         }
 
-        if (!renderInDocumentSize)
-        {
-            double lengthDiff = target.LocalClipBounds.Size.Length -
-                                cachedTexture.DrawingSurface.LocalClipBounds.Size.Length;
-            if (lengthDiff > 0 || target.LocalClipBounds.Pos != cachedTexture.DrawingSurface.LocalClipBounds.Pos ||
-                lengthDiff < -ZoomDiffToRerender)
-            {
-                return true;
-            }
-        }
-
-        int currentGraphCacheHash = finalGraph.GetCacheHash();
-        if (lastGraphCacheHash != currentGraphCacheHash)
-        {
-            lastGraphCacheHash = currentGraphCacheHash;
-            return true;
-        }
 
         return false;
     }
 
-    private Matrix3X3 SolveMatrixDiff(DrawingSurface target, Texture cachedTexture)
-    {
-        Matrix3X3 old = cachedTexture.DrawingSurface.Canvas.TotalMatrix;
-        Matrix3X3 current = target.Canvas.TotalMatrix;
-
-        Matrix3X3 solveMatrixDiff = current.Concat(old.Invert());
-        return solveMatrixDiff;
-    }
-
-
     private bool HighDpiRenderNodePresent(IReadOnlyNodeGraph documentNodeGraph)
     {
         bool highDpiRenderNodePresent = false;
@@ -265,61 +382,36 @@ internal class SceneRenderer : IDisposable
 
         return highDpiRenderNodePresent;
     }
+}
 
-    private void RenderOnionSkin(DrawingSurface target, ChunkResolution resolution, SamplingOptions sampling, string? targetOutput)
+readonly struct RenderState
+{
+    public ChunkResolution ChunkResolution { get; init; }
+    public bool HighResRendering { get; init; }
+    public string TargetOutput { get; init; }
+    public int GraphCacheHash { get; init; }
+    public RectD VisibleDocumentRegion { get; init; }
+    public double ZoomLevel { get; init; }
+    public int OnionFrames { get; init; }
+    public double OnionOpacity { get; init; }
+    public bool OnionSkinning { get; init; }
+
+    public bool ShouldRerender(RenderState other)
     {
-        var animationData = Document.AnimationData;
-        if (!DocumentViewModel.AnimationHandler.OnionSkinningEnabledBindable)
-        {
-            return;
-        }
-
-        double onionOpacity = animationData.OnionOpacity / 100.0;
-        double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
-
-        var finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
-        var renderOutputSize = SolveRenderOutputSize(targetOutput, finalGraph, Document.Size);
-
-        // Render previous frames'
-        for (int i = 1; i <= animationData.OnionFrames; i++)
-        {
-            int frame = DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame - i;
-            if (frame < DocumentViewModel.AnimationHandler.FirstVisibleFrame)
-            {
-                break;
-            }
-
-            double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
-
-
-            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size,
-                Document.ProcessingColorSpace, sampling, finalOpacity);
-            onionContext.TargetOutput = targetOutput;
-            finalGraph.Execute(onionContext);
-        }
-
-        // Render next frames
-        for (int i = 1; i <= animationData.OnionFrames; i++)
-        {
-            int frame = DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame + i;
-            if (frame >= DocumentViewModel.AnimationHandler.LastFrame)
-            {
-                break;
-            }
+        return !ChunkResolution.Equals(other.ChunkResolution) || HighResRendering != other.HighResRendering ||
+               TargetOutput != other.TargetOutput || GraphCacheHash != other.GraphCacheHash ||
+               OnionFrames != other.OnionFrames || Math.Abs(OnionOpacity - other.OnionOpacity) > 0.05 ||
+               OnionSkinning != other.OnionSkinning ||
+               VisibleRegionChanged(other) || ZoomDiff(other) > 0;
+    }
 
-            double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
-            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size,
-                Document.ProcessingColorSpace, sampling, finalOpacity);
-            onionContext.TargetOutput = targetOutput;
-            finalGraph.Execute(onionContext);
-        }
+    private bool VisibleRegionChanged(RenderState other)
+    {
+        return !other.VisibleDocumentRegion.IsFullyInside(VisibleDocumentRegion);
     }
 
-    public void Dispose()
+    private double ZoomDiff(RenderState other)
     {
-        foreach (var texture in cachedTextures)
-        {
-            texture.Value?.Dispose();
-        }
+        return Math.Abs(ZoomLevel - other.ZoomLevel);
     }
 }

+ 5 - 0
src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs

@@ -83,4 +83,9 @@ public class ByteBuilder
     {
         _data.Add(value ? (byte)1 : (byte)0);
     }
+
+    public void AddByteArray(byte[] serialized)
+    {
+        _data.AddRange(serialized);
+    }
 }

+ 7 - 0
src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs

@@ -122,4 +122,11 @@ public class ByteExtractor
 
         return sb.ToString();
     }
+
+    public Span<byte> GetByteSpan(int length)
+    {
+        Span<byte> span = new Span<byte>(_data, Position, length);
+        Position += length;
+        return span;
+    }
 }

+ 61 - 18
src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs

@@ -1,49 +1,92 @@
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.Extensions.CommonApi.Utilities;
 
 namespace PixiEditor.Models.Serialization.Factories;
 
 public class ChunkyImageSerializationFactory : SerializationFactory<byte[], ChunkyImage>
 {
-    private static SurfaceSerializationFactory surfaceFactory = new();
-
     public override byte[] Serialize(ChunkyImage original)
     {
-        var encoder = Config.Encoder;
+        var chunks = original.CloneAllCommitedNonEmptyChunks();
+        ByteBuilder builder = new();
+        builder.AddVecD(original.CommittedSize);
+
+        SurfaceSerializationFactory surfaceFactory = new();
         surfaceFactory.Config = Config;
 
-        using Surface surface = new Surface(original.LatestSize);
-        original.DrawMostUpToDateRegionOn(
-            new RectI(0, 0, original.LatestSize.X,
-                original.LatestSize.Y), ChunkResolution.Full, surface.DrawingSurface, new VecI(0, 0), new Paint());
+        builder.AddInt(chunks.Count);
+        foreach (var chunk in chunks)
+        {
+            builder.AddVecD(chunk.Key);
+            byte[] serialized = surfaceFactory.Serialize(chunk.Value);
+            builder.AddInt(serialized.Length);
+            builder.AddByteArray(serialized);
+        }
 
-        return surfaceFactory.Serialize(surface);
+        return builder.Build();
     }
 
     public override bool TryDeserialize(object serialized, out ChunkyImage original,
         (string serializerName, string serializerVersion) serializerData)
     {
-        if (serialized is byte[] imgBytes)
+        SurfaceSerializationFactory surfaceFactory = new();
+        surfaceFactory.Config = Config;
+        if (IsFilePreVersion(serializerData, new Version(2, 0, 1, 17)) || serializerData == default)
+        {
+            if (serialized is byte[] imgBytes)
+            {
+                if (!surfaceFactory.TryDeserialize(imgBytes, out Surface surface, serializerData))
+                {
+                    original = null;
+                    return false;
+                }
+
+                original = new ChunkyImage(surface.Size, Config.ProcessingColorSpace);
+                original.EnqueueDrawImage(VecI.Zero, surface);
+                original.CommitChanges();
+                surface.Dispose();
+                return true;
+            }
+
+            original = null;
+            return false;
+        }
+
+        if (serialized is not byte[] bytes)
+        {
+            original = null;
+            return false;
+        }
+
+        ByteExtractor byteExtractor = new(bytes);
+        VecD size = byteExtractor.GetVecD();
+        original = new ChunkyImage((VecI)size, Config.ProcessingColorSpace);
+        int chunkCount = byteExtractor.GetInt();
+
+        for (int i = 0; i < chunkCount; i++)
         {
-            surfaceFactory.Config = Config;
-            if (!surfaceFactory.TryDeserialize(imgBytes, out Surface surface, serializerData))
+            VecD chunkPos = byteExtractor.GetVecD();
+            int chunkDataLength = byteExtractor.GetInt();
+            Span<byte> chunkData = byteExtractor.GetByteSpan(chunkDataLength);
+            if (!surfaceFactory.TryDeserialize(chunkData, out Surface chunkSurface, serializerData))
             {
+                original.Dispose();
                 original = null;
                 return false;
             }
 
-            original = new ChunkyImage(surface.Size, Config.ProcessingColorSpace);
-            original.EnqueueDrawImage(VecI.Zero, surface);
-            original.CommitChanges();
-            surface.Dispose();
-            return true;
+            RectD chunkRect = new RectD(chunkPos * chunkSurface.Size.X, chunkSurface.Size);
+            original.EnqueueDrawImage((VecI)chunkRect.TopLeft, chunkSurface);
+            chunkSurface.Dispose();
         }
 
-        original = null;
-        return false;
+        original.CommitChanges();
+        return true;
     }
 
     public override string DeserializationId { get; } = "PixiEditor.ChunkyImage";

+ 15 - 4
src/PixiEditor/Models/Serialization/Factories/SurfaceSerializationFactory.cs

@@ -22,7 +22,7 @@ public class SurfaceSerializationFactory : SerializationFactory<byte[], Surface>
     {
         if (serialized is byte[] imgBytes)
         {
-            original = DecodeSurface(imgBytes, Config.Encoder, Config.ProcessingColorSpace);
+            original = DecodeSurface(imgBytes, Config.Encoder);
             return true;
         }
 
@@ -30,11 +30,23 @@ public class SurfaceSerializationFactory : SerializationFactory<byte[], Surface>
         return false;
     }
 
+    public bool TryDeserialize(Span<byte> serialized, out Surface original,
+        (string serializerName, string serializerVersion) serializerData)
+    {
+        original = DecodeSurface(serialized, Config.Encoder);
+        return true;
+    }
+
 
-    public static Surface DecodeSurface(byte[] imgBytes, ImageEncoder encoder, ColorSpace processingColorSpace)
+    public static Surface DecodeSurface(byte[] imgBytes, ImageEncoder encoder)
+    {
+        return DecodeSurface(imgBytes.AsSpan(), encoder);
+    }
+
+    public static Surface DecodeSurface(Span<byte> imgSpan, ImageEncoder encoder)
     {
         byte[] decoded =
-            encoder.Decode(imgBytes, out SKImageInfo info);
+            encoder.Decode(imgSpan, out SKImageInfo info);
         ImageInfo finalInfo = info.ToImageInfo();
 
         using Image img = Image.FromPixels(finalInfo, decoded);
@@ -46,6 +58,5 @@ public class SurfaceSerializationFactory : SerializationFactory<byte[], Surface>
         return surface;
     }
 
-
     public override string DeserializationId { get; } = "PixiEditor.Surface";
 }

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.1.16")]
-[assembly: AssemblyFileVersion("2.0.1.16")]
+[assembly: AssemblyVersion("2.0.1.17")]
+[assembly: AssemblyFileVersion("2.0.1.17")]

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

@@ -36,10 +36,10 @@
                                 </ImageBrush.Transform>
                             </ImageBrush>
                         </Border.Background>
-                        <visuals:PreviewPainterControl
-                            PreviewPainter="{Binding Item.PreviewPainter, RelativeSource={RelativeSource TemplatedParent}}"
+                        <visuals:PreviewTextureControl
+                            TexturePreview="{Binding Item.PreviewTexture, RelativeSource={RelativeSource TemplatedParent}}"
                             Width="60" Height="60" RenderOptions.BitmapInterpolationMode="None">
-                            </visuals:PreviewPainterControl>
+                        </visuals:PreviewTextureControl>
                     </Border>
                 </Grid>
             </ControlTemplate>

+ 1 - 1
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -59,7 +59,7 @@
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                     SocketDropCommand="{Binding SocketDropCommand,
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                    ResultPreview="{Binding ResultPainter}"
+                                    ResultPreview="{Binding Preview}"
                                     Bounds="{Binding UiSize, Mode=OneWayToSource}" />
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>

+ 18 - 8
src/PixiEditor/Styles/Templates/NodeView.axaml

@@ -3,7 +3,7 @@
                     xmlns:nodes="clr-namespace:PixiEditor.Views.Nodes"
                     xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
                     xmlns:ui="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
-                    >
+                    xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters">
     <ControlTheme TargetType="nodes:NodeView" x:Key="{x:Type nodes:NodeView}">
         <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" />
         <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
@@ -47,7 +47,8 @@
                                         </ControlTheme>
                                     </ItemsControl.ItemContainerTheme>
                                 </ItemsControl>
-                                <ItemsControl Name="PART_Inputs" ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
+                                <ItemsControl Name="PART_Inputs" ItemsSource="{TemplateBinding Inputs}"
+                                              ClipToBounds="False">
                                     <ItemsControl.ItemContainerTheme>
                                         <ControlTheme TargetType="ContentPresenter">
                                             <Setter Property="DataContext" Value="." />
@@ -56,18 +57,27 @@
                                 </ItemsControl>
                             </StackPanel>
                         </Border>
-                        <Border IsVisible="{Binding CanRenderPreview, RelativeSource={RelativeSource TemplatedParent}, FallbackValue=False}"
-                                CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
+                        <Border
+                            CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
+                            <Border.IsVisible>
+                                <MultiBinding Converter="{converters:ResultPreviewIsPresentConverter}">
+                                    <Binding Path="ResultPreview"
+                                             RelativeSource="{RelativeSource TemplatedParent}" />
+                                    <Binding Path="ResultPreview.Preview"
+                                             RelativeSource="{RelativeSource TemplatedParent}" />
+                                </MultiBinding>
+                            </Border.IsVisible>
                             <Panel RenderOptions.BitmapInterpolationMode="None" Width="200" Height="200">
                                 <Panel.Background>
                                     <ImageBrush Source="/Images/CheckerTile.png"
                                                 TileMode="Tile" DestinationRect="0, 0, 25, 25" />
                                 </Panel.Background>
-                                <visuals:PreviewPainterControl
-                                    PreviewPainter="{TemplateBinding ResultPreview}"
-                                    FrameToRender="{TemplateBinding ActiveFrame}"
+                                <visuals:PreviewTextureControl
+                                    Width="200"
+                                    Height="200"
+                                    TexturePreview="{TemplateBinding ResultPreview}"
                                     RenderOptions.BitmapInterpolationMode="None">
-                                </visuals:PreviewPainterControl>
+                                </visuals:PreviewTextureControl>
                             </Panel>
                         </Border>
                     </Grid>

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

@@ -25,8 +25,8 @@
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:PreviewPainterControl
-                                PreviewPainter="{Binding Item.PreviewPainter, RelativeSource={RelativeSource TemplatedParent}}"
+                            <visuals:PreviewTextureControl
+                                TexturePreview="{Binding Item.PreviewTexture, RelativeSource={RelativeSource TemplatedParent}}"
                                 Width="60" Height="60" RenderOptions.BitmapInterpolationMode="None"/>
                         </Border>
                         <TextBlock Margin="5 0 0 0" VerticalAlignment="Center" Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />

+ 8 - 8
src/PixiEditor/ViewModels/Document/CelViewModel.cs

@@ -10,12 +10,18 @@ namespace PixiEditor.ViewModels.Document;
 
 internal abstract class CelViewModel : ObservableObject, ICelHandler
 {
-    private PreviewPainter? previewPainter;
     private int startFrameBindable;
     private int durationBindable;
     private bool isVisibleBindable = true;
     private bool isSelected;
     private bool isCollapsed;
+    private TexturePreview? previewTexture;
+
+    public TexturePreview? PreviewTexture
+    {
+        get => previewTexture;
+        set => SetProperty(ref previewTexture, value);
+    }
 
     public bool IsCollapsed
     {
@@ -29,11 +35,6 @@ internal abstract class CelViewModel : ObservableObject, ICelHandler
 
     IDocument ICelHandler.Document => Document;
 
-    public PreviewPainter? PreviewPainter
-    {
-        get => previewPainter;
-        set => SetProperty(ref previewPainter, value);
-    }
 
     public virtual int StartFrameBindable
     {
@@ -147,7 +148,6 @@ internal abstract class CelViewModel : ObservableObject, ICelHandler
 
     public void Dispose()
     {
-        PreviewPainter?.Dispose();
-        PreviewPainter = null;
+        PreviewTexture?.Preview?.Dispose();
     }
 }

+ 5 - 23
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -185,27 +185,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
 
-    private PreviewPainter miniPreviewPainter;
-
-    public PreviewPainter MiniPreviewPainter
-    {
-        get => miniPreviewPainter;
-        set
-        {
-            SetProperty(ref miniPreviewPainter, value);
-        }
-    }
-
-    private PreviewPainter previewSurface;
-
-    public PreviewPainter PreviewPainter
-    {
-        get => previewSurface;
-        set
-        {
-            SetProperty(ref previewSurface, value);
-        }
-    }
+    public Dictionary<Guid, Texture> SceneTextures { get; } = new();
 
     private VectorPath selectionPath = new VectorPath();
     public VectorPath SelectionPathBindable => selectionPath;
@@ -222,7 +202,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public AnimationDataViewModel AnimationDataViewModel { get; }
     public TextOverlayViewModel TextOverlayViewModel { get; }
 
-
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers => softSelectedStructureMembers;
     private DocumentInternalParts Internals { get; }
     INodeGraphHandler IDocument.NodeGraphHandler => NodeGraph;
@@ -1321,7 +1300,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         {
             NodeGraph.Dispose();
             Renderer.Dispose();
-            SceneRenderer.Dispose();
+            foreach (var texture in SceneTextures)
+            {
+                texture.Value?.Dispose();
+            }
             AnimationDataViewModel.Dispose();
             Internals.ChangeController.TryStopActiveExecutor();
             Internals.Tracker.Dispose();

+ 15 - 24
src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs

@@ -76,6 +76,21 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
         OnPropertyChanged(nameof(MaskIsVisibleBindable));
     }
 
+    private TexturePreview? maskPreview;
+    private TexturePreview? preview;
+
+    public TexturePreview? Preview
+    {
+        get => preview;
+        set => SetProperty(ref preview, value);
+    }
+
+    public TexturePreview? MaskPreview
+    {
+        get => maskPreview;
+        set => SetProperty(ref maskPreview, value);
+    }
+
     public bool MaskIsVisibleBindable
     {
         get => maskIsVisible;
@@ -167,31 +182,7 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
         set => SetProperty(ref selection, value);
     }
 
-    private PreviewPainter? previewSurface;
-    private PreviewPainter? _maskPreviewPainter;
-
-    public PreviewPainter? PreviewPainter
-    {
-        get => previewSurface;
-        set => SetProperty(ref previewSurface, value);
-    }
-
-    public PreviewPainter? MaskPreviewPainter
-    {
-        get => _maskPreviewPainter;
-        set => SetProperty(ref _maskPreviewPainter, value);
-    }
-
     IDocument IStructureMemberHandler.Document => Document;
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        PreviewPainter?.Dispose();
-        MaskPreviewPainter?.Dispose();
-        PreviewPainter = null;
-        MaskPreviewPainter = null;
-    }
 }
 
 public static class StructureMemberViewModel

+ 75 - 0
src/PixiEditor/ViewModels/Document/TexturePreview.cs

@@ -0,0 +1,75 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Drawie.Backend.Core;
+using Drawie.Numerics;
+
+namespace PixiEditor.ViewModels.Document;
+
+public class TexturePreview : ObservableObject
+{
+    private Texture preview;
+
+    public Guid Id { get; }
+
+    public Guid? SubId { get; init; }
+
+    public Texture Preview
+    {
+        get => preview;
+        set
+        {
+            bool updated = SetProperty(ref preview, value);
+            if(updated)
+                InvokeTextureUpdated();
+        }
+    }
+
+    public Dictionary<object, Func<VecI>> Listeners { get; } = new();
+
+    public event Action TextureUpdated;
+
+    private Action<Guid, Guid?> requestRender;
+
+    public TexturePreview(Guid forId, Action<Guid> requestRender)
+    {
+        Id = forId;
+        this.requestRender = (id, _) => requestRender(id);
+    }
+
+    public TexturePreview(Guid forId, Guid subId, Action<Guid, Guid?> requestRender)
+    {
+        Id = forId;
+        SubId = subId;
+        this.requestRender = requestRender;
+    }
+
+    public void Attach(object source, Func<VecI> getSize)
+    {
+        if(Listeners.TryAdd(source, getSize))
+            requestRender(Id, SubId);
+    }
+
+    public void Detach(object source)
+    {
+        Listeners.Remove(source);
+    }
+
+    public void InvokeTextureUpdated()
+    {
+        TextureUpdated?.Invoke();
+    }
+
+    public VecI GetMaxListenerSize()
+    {
+        VecI maxSize = VecI.Zero;
+        foreach (var sizeFunc in Listeners.Values)
+        {
+            VecI size = sizeFunc();
+            if (size.Length > maxSize.Length)
+            {
+                maxSize = size;
+            }
+        }
+
+        return maxSize;
+    }
+}

+ 6 - 8
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -30,14 +30,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     private IBrush? categoryBrush;
     private string? nodeNameBindable;
     private VecD position;
+    private TexturePreview? resultTexture;
     private ObservableRangeCollection<INodePropertyHandler> inputs;
     private ObservableRangeCollection<INodePropertyHandler> outputs;
-    private PreviewPainter resultPainter;
     private bool isSelected;
     private string? icon;
 
     protected Guid id;
 
+    public TexturePreview? Preview
+    {
+        get => resultTexture;
+        set => SetProperty(ref resultTexture, value);
+    }
     public IReadOnlyDictionary<string, INodePropertyHandler> InputPropertyMap => inputPropertyMap;
     public IReadOnlyDictionary<string, INodePropertyHandler> OutputPropertyMap => outputPropertyMap;
 
@@ -163,12 +168,6 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
-    public PreviewPainter ResultPainter
-    {
-        get => resultPainter;
-        set => SetProperty(ref resultPainter, value);
-    }
-
     public bool IsNodeSelected
     {
         get => isSelected;
@@ -507,7 +506,6 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
     public virtual void Dispose()
     {
-        ResultPainter?.Dispose();
     }
 
     public NodePropertyViewModel FindInputProperty(string propName)

+ 25 - 10
src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -146,7 +146,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         }
     }
 
-    private PreviewPainterControl previewPainterControl;
+    private TextureControl previewPainterControl;
 
     public void IndexChanged()
     {
@@ -175,9 +175,18 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         PixiEditorSettings.Scene.PrimaryBackgroundColor.ValueChanged += UpdateBackgroundBitmap;
         PixiEditorSettings.Scene.SecondaryBackgroundColor.ValueChanged += UpdateBackgroundBitmap;
 
-        previewPainterControl = new PreviewPainterControl(
-            Document.MiniPreviewPainter,
-            Document.AnimationDataViewModel.ActiveFrameTime.Frame);
+        previewPainterControl = new TextureControl();
+        var nonZoomed = Document.SceneTextures.Where(x =>
+            x.Value is { DrawingSurface.Canvas.TotalMatrix: { TransX: 0, TransY: 0, SkewX: 0, SkewY: 0 } }).ToArray();
+        if (nonZoomed.Length > 0)
+        {
+            var minSize = nonZoomed.MinBy(x => x.Value.Size);
+            if (minSize.Value != null)
+            {
+                previewPainterControl.Texture = minSize.Value;
+            }
+        }
+
         TabCustomizationSettings.Icon = previewPainterControl;
     }
 
@@ -188,13 +197,19 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         {
             OnPropertyChanged(nameof(Title));
         }
-        else if (e.PropertyName == nameof(DocumentViewModel.MiniPreviewPainter))
-        {
-            previewPainterControl.PreviewPainter = Document.MiniPreviewPainter;
-            previewPainterControl.FrameToRender = Document.AnimationDataViewModel.ActiveFrameTime.Frame;
-        }
         else if (e.PropertyName == nameof(DocumentViewModel.AllChangesSaved))
         {
+            var nonZoomed = Document.SceneTextures.Where(x =>
+                    x.Value is { DrawingSurface.Canvas.TotalMatrix: { TransX: 0, TransY: 0, SkewX: 0, SkewY: 0 } })
+                .ToArray();
+            if (nonZoomed.Length > 0)
+            {
+                var minSize = nonZoomed.MinBy(x => x.Value.Size);
+                if (minSize.Value != null)
+                {
+                    previewPainterControl.Texture = minSize.Value;
+                }
+            }
             TabCustomizationSettings.SavedState = GetSaveState(Document);
         }
         else if (e.PropertyName == nameof(DocumentViewModel.AllChangesAutosaved))
@@ -254,7 +269,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
 
     public VecI GetRenderOutputSize()
     {
-       return Document.GetRenderOutputSize(RenderOutputName);
+        return Document.GetRenderOutputSize(RenderOutputName);
     }
 
     private void UpdateBackgroundBitmap(Setting<string> setting, string newValue)

+ 2 - 0
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
+using Drawie.Backend.Core.Bridge;
 using Microsoft.Extensions.DependencyInjection;
 using Drawie.Backend.Core.ColorsImpl;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
@@ -346,6 +347,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
 
         if (result != ConfirmationType.Canceled)
         {
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
             BeforeDocumentClosed?.Invoke(document);
             if (!DocumentManagerSubViewModel.Documents.Remove(document))
                 throw new InvalidOperationException(

+ 1 - 0
src/PixiEditor/Views/Dock/DocumentPreviewDockView.axaml

@@ -12,5 +12,6 @@
         <dock:DocumentPreviewDockViewModel/>
     </Design.DataContext>
     <main:DocumentPreview Document="{Binding DocumentManagerSubViewModel.ActiveDocument}"
+                          MaxBilinearSamplingSize="{Binding DocumentManagerSubViewModel.Owner.ViewportSubViewModel.MaxBilinearSampleSize}"
                      PrimaryColor="{Binding ColorsSubViewModel.PrimaryColor, Mode=TwoWay, Converter={converters:GenericColorToMediaColorConverter}}"/>
 </UserControl>

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

@@ -71,10 +71,9 @@
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:PreviewPainterControl
-                                PreviewPainter="{Binding Folder.PreviewPainter, ElementName=folderControl}"
+                            <visuals:PreviewTextureControl
+                                TexturePreview="{Binding Folder.Preview, ElementName=folderControl}"
                                 ClipToBounds="True"
-                                FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
                                 Width="30" Height="30" RenderOptions.BitmapInterpolationMode="None" />
                         </Border>
                         <Border
@@ -92,11 +91,10 @@
                                 </ImageBrush>
                             </Border.Background>
                             <Grid IsHitTestVisible="False">
-                                <visuals:PreviewPainterControl
-                                    PreviewPainter="{Binding Folder.MaskPreviewPainter, ElementName=folderControl}"
+                                <visuals:PreviewTextureControl
+                                    TexturePreview="{Binding Folder.MaskPreview, ElementName=folderControl}"
                                     Width="30" Height="30"
                                     ClipToBounds="True"
-                                    FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
                                     RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
                                 <Path
                                     Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z"

+ 5 - 7
src/PixiEditor/Views/Layers/LayerControl.axaml

@@ -80,10 +80,9 @@
                                 <Binding ElementName="uc" Path="Layer.HasMaskBindable" />
                             </MultiBinding>
                         </Border.BorderBrush>
-                        <visuals:PreviewPainterControl 
-                            ClipToBounds="True" 
-                            PreviewPainter="{Binding Layer.PreviewPainter, ElementName=uc}"
-                            FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=uc}"
+                        <visuals:PreviewTextureControl
+                            ClipToBounds="True"
+                            TexturePreview="{Binding Layer.Preview, ElementName=uc}"
                             Width="30" Height="30"
                             RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
                     </Border>
@@ -107,11 +106,10 @@
                             </MultiBinding>
                         </Border.BorderBrush>
                         <Grid IsHitTestVisible="False">
-                             <visuals:PreviewPainterControl 
-                                    PreviewPainter="{Binding Layer.MaskPreviewPainter, ElementName=uc}" 
+                             <visuals:PreviewTextureControl
+                                    TexturePreview="{Binding Layer.MaskPreview, ElementName=uc}"
                                     Width="30" Height="30"
                                     ClipToBounds="True"
-                                    FrameToRender="{Binding Path=Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=uc}"
                                     RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                             <Path
                                 Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z"

+ 1 - 0
src/PixiEditor/Views/Main/DocumentPreview.axaml

@@ -26,6 +26,7 @@
             <viewportControls:FixedViewport
                 Delayed="True"
                 x:Name="viewport"
+                MaxBilinearSampleSize="{Binding ElementName=uc, Path=MaxBilinearSamplingSize}"
                 RenderInDocSize="{Binding ElementName=highDpiButton, Path=IsChecked}"
                 Document="{Binding Document, ElementName=uc}"
                 Background="{Binding ActiveItem.Value, ElementName=backgroundButton}" />

+ 8 - 0
src/PixiEditor/Views/Main/DocumentPreview.axaml.cs

@@ -24,6 +24,14 @@ internal partial class DocumentPreview : UserControl
     public static readonly StyledProperty<Color> PrimaryColorProperty =
         AvaloniaProperty.Register<DocumentPreview, Color>(nameof(PrimaryColor));
 
+    public static readonly StyledProperty<int> MaxBilinearSamplingSizeProperty = AvaloniaProperty.Register<DocumentPreview, int>(
+        nameof(MaxBilinearSamplingSize), 4096);
+
+    public int MaxBilinearSamplingSize
+    {
+        get => GetValue(MaxBilinearSamplingSizeProperty);
+        set => SetValue(MaxBilinearSamplingSizeProperty, value);
+    }
     public DocumentViewModel Document
     {
         get => GetValue(DocumentProperty);

+ 10 - 15
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml

@@ -8,24 +8,19 @@
              xmlns:ui="clr-namespace:PixiEditor.Helpers.UI"
              xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
              xmlns:ui1="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:viewportControls="clr-namespace:PixiEditor.Views.Main.ViewportControls"
              mc:Ignorable="d"
              x:Name="uc"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"
              d:DesignHeight="450" d:DesignWidth="800">
-    
-        <visuals:PreviewPainterControl
-            x:Name="mainImage"
-            Focusable="True"
-            PreviewPainter="{Binding Document.PreviewPainter, ElementName=uc}"
-            CustomRenderSize="{Binding CustomRenderSize, ElementName=uc}"
-            FrameToRender="{Binding Document.AnimationDataViewModel.ActiveFrameBindable, ElementName=uc}"
-            SizeChanged="OnImageSizeChanged">
-            <ui1:RenderOptionsBindable.BitmapInterpolationMode>
-                <MultiBinding Converter="{converters1:WidthToBitmapScalingModeConverter}">
-                    <Binding ElementName="uc" Path="Document.SizeBindable.X" />
-                    <Binding ElementName="mainImage" Path="Bounds.Width" />
-                </MultiBinding>
-            </ui1:RenderOptionsBindable.BitmapInterpolationMode>
-        </visuals:PreviewPainterControl>
+
+    <visuals:TextureControl
+        x:Name="mainImage"
+        Focusable="True"
+        Stretch="Uniform"
+        Texture="{Binding  SceneTexture,
+        RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:FixedViewport}}"
+        SizeChanged="OnImageSizeChanged">
+    </visuals:TextureControl>
 </UserControl>

+ 58 - 14
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs

@@ -7,6 +7,8 @@ using Avalonia.Media;
 using Avalonia.Threading;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Position;
 using Drawie.Numerics;
@@ -23,11 +25,22 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<bool> DelayedProperty =
         AvaloniaProperty.Register<FixedViewport, bool>(nameof(Delayed), false);
 
-    public static readonly StyledProperty<bool> RenderInDocSizeProperty = AvaloniaProperty.Register<FixedViewport, bool>(
-        nameof(RenderInDocSize));
+    public static readonly StyledProperty<bool> RenderInDocSizeProperty =
+        AvaloniaProperty.Register<FixedViewport, bool>(
+            nameof(RenderInDocSize));
 
-    public static readonly StyledProperty<VecI> CustomRenderSizeProperty = AvaloniaProperty.Register<FixedViewport, VecI>(
-        nameof(CustomRenderSize));
+    public static readonly StyledProperty<VecI> CustomRenderSizeProperty =
+        AvaloniaProperty.Register<FixedViewport, VecI>(
+            nameof(CustomRenderSize));
+
+    public static readonly StyledProperty<int> MaxBilinearSampleSizeProperty = AvaloniaProperty.Register<FixedViewport, int>(
+        nameof(MaxBilinearSampleSize));
+
+    public int MaxBilinearSampleSize
+    {
+        get => GetValue(MaxBilinearSampleSizeProperty);
+        set => SetValue(MaxBilinearSampleSizeProperty, value);
+    }
 
     public VecI CustomRenderSize
     {
@@ -55,6 +68,8 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DocumentProperty, value);
     }
 
+    public Texture? SceneTexture => Document?.SceneTextures.TryGetValue(GuidValue, out var tex) == true ? tex : null;
+
     public Guid GuidValue { get; } = Guid.NewGuid();
 
     static FixedViewport()
@@ -66,8 +81,6 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public FixedViewport()
     {
         InitializeComponent();
-        Loaded += OnLoad;
-        Unloaded += OnUnload;
     }
 
     protected override Size MeasureOverride(Size availableSize)
@@ -80,18 +93,18 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
             height = availableSize.Height;
             width = height * aspectRatio;
         }
-        
+
         return new Size(width, height);
     }
 
-    private void OnUnload(object sender, RoutedEventArgs e)
+    protected override void OnLoaded(RoutedEventArgs e)
     {
-        Document?.Operations.RemoveViewport(GuidValue);
+        Document?.Operations.AddOrUpdateViewport(GetLocation());
     }
 
-    private void OnLoad(object sender, RoutedEventArgs e)
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
     {
-        Document?.Operations.AddOrUpdateViewport(GetLocation());
+        Document?.Operations.RemoveViewport(GuidValue);
     }
 
     private ChunkResolution CalculateResolution()
@@ -110,6 +123,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
 
     private void ForceRefreshFinalImage()
     {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SceneTexture)));
         mainImage.InvalidateVisual();
     }
 
@@ -119,17 +133,42 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         if (Document is not null)
             docSize = Document.SizeBindable;
 
+        Matrix3X3 scaling = Matrix3X3.CreateScale((float)Bounds.Width / (float)docSize.X,
+            (float)Bounds.Height / (float)docSize.Y);
+
         return new ViewportInfo(
             0,
             docSize / 2,
-            new VecD(mainImage.Bounds.Width, mainImage.Bounds.Height),
+            new VecD(Bounds.Width, Bounds.Height),
+            scaling,
+            null,
+            "DEFAULT",
+            CalculateSampling(),
             docSize,
             CalculateResolution(),
             GuidValue,
             Delayed,
+            false,
             ForceRefreshFinalImage);
     }
 
+    internal SamplingOptions CalculateSampling()
+    {
+        if (Document == null)
+            return SamplingOptions.Default;
+
+        if (Document.SizeBindable.LongestAxis > MaxBilinearSampleSize || SceneTexture == null)
+        {
+            return SamplingOptions.Default;
+        }
+
+        VecD densityVec = ((VecD)SceneTexture.Size).Divide(new VecD(Bounds.Width, Bounds.Height));
+        double density = Math.Min(densityVec.X, densityVec.Y);
+        return density > 1
+            ? SamplingOptions.Bilinear
+            : SamplingOptions.Default;
+    }
+
     private static void OnDocumentChange(AvaloniaPropertyChangedEventArgs<DocumentViewModel> args)
     {
         DocumentViewModel? oldDoc = args.OldValue.Value;
@@ -143,11 +182,12 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         {
             oldDoc.SizeChanged -= viewport.DocSizeChanged;
         }
+
         if (newDoc != null)
         {
             newDoc.SizeChanged += viewport.DocSizeChanged;
         }
-        
+
         viewport.ForceRefreshFinalImage();
     }
 
@@ -169,5 +209,9 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     {
         Document?.Operations.AddOrUpdateViewport(GetLocation());
     }
-}
 
+    protected override void OnSizeChanged(SizeChangedEventArgs e)
+    {
+        Document?.Operations.AddOrUpdateViewport(GetLocation());
+    }
+}

+ 1 - 0
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -253,6 +253,7 @@
             FadeOut="{Binding Source={viewModels:ToolVM ColorPickerToolViewModel}, Path=PickOnlyFromReferenceLayer, Mode=OneWay}"
             DefaultCursor="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ToolCursor, Mode=OneWay}"
             RenderOutput="{Binding ViewportRenderOutput, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
+            ViewportId="{Binding GuidValue, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
             AutoBackgroundScale="{Binding AutoBackgroundScale, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
             CustomBackgroundScaleX="{Binding CustomBackgroundScaleX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
             CustomBackgroundScaleY="{Binding CustomBackgroundScaleY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"

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

@@ -5,6 +5,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.Skia;
 using Avalonia.VisualTree;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
@@ -20,6 +21,7 @@ using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Position;
 using Drawie.Numerics;
+using Drawie.Skia;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.UI.Common.Behaviors;
 using PixiEditor.ViewModels.Document;
@@ -119,14 +121,17 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<bool> AutoBackgroundScaleProperty = AvaloniaProperty.Register<Viewport, bool>(
         nameof(AutoBackgroundScale), true);
 
-    public static readonly StyledProperty<double> CustomBackgroundScaleXProperty = AvaloniaProperty.Register<Viewport, double>(
-        nameof(CustomBackgroundScaleX));
+    public static readonly StyledProperty<double> CustomBackgroundScaleXProperty =
+        AvaloniaProperty.Register<Viewport, double>(
+            nameof(CustomBackgroundScaleX));
 
-    public static readonly StyledProperty<double> CustomBackgroundScaleYProperty = AvaloniaProperty.Register<Viewport, double>(
-        nameof(CustomBackgroundScaleY));
+    public static readonly StyledProperty<double> CustomBackgroundScaleYProperty =
+        AvaloniaProperty.Register<Viewport, double>(
+            nameof(CustomBackgroundScaleY));
 
-    public static readonly StyledProperty<Bitmap> BackgroundBitmapProperty = AvaloniaProperty.Register<Viewport, Bitmap>(
-        nameof(BackgroundBitmap));
+    public static readonly StyledProperty<Bitmap> BackgroundBitmapProperty =
+        AvaloniaProperty.Register<Viewport, Bitmap>(
+            nameof(BackgroundBitmap));
 
     public Bitmap BackgroundBitmap
     {
@@ -139,6 +144,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         get => GetValue(CustomBackgroundScaleYProperty);
         set => SetValue(CustomBackgroundScaleYProperty, value);
     }
+
     public double CustomBackgroundScaleX
     {
         get => GetValue(CustomBackgroundScaleXProperty);
@@ -382,6 +388,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private MouseUpdateController? mouseUpdateController;
     private ViewportOverlays builtInOverlays = new();
+
     public static readonly StyledProperty<int> MaxBilinearSamplingSizeProperty
         = AvaloniaProperty.Register<Viewport, int>("MaxBilinearSamplingSize", 4096);
 
@@ -391,6 +398,11 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     static Viewport()
     {
         DocumentProperty.Changed.Subscribe(OnDocumentChange);
+        ViewportRenderOutputProperty.Changed.Subscribe(e =>
+        {
+            Viewport? viewport = (Viewport)e.Sender;
+            viewport.Document?.Operations.AddOrUpdateViewport(viewport.GetLocation());
+        });
         ZoomViewportTriggerProperty.Changed.Subscribe(ZoomViewportTriggerChanged);
         CenterViewportTriggerProperty.Changed.Subscribe(CenterViewportTriggerChanged);
         HighResPreviewProperty.Changed.Subscribe(OnHighResPreviewChanged);
@@ -512,8 +524,11 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private ViewportInfo GetLocation()
     {
-        return new(AngleRadians, Center, RealDimensions, Dimensions, CalculateResolution(), GuidValue, Delayed,
-            ForceRefreshFinalImage);
+        return new(AngleRadians, Center, RealDimensions,
+            Scene.CalculateTransformMatrix().ToSKMatrix().ToMatrix3X3(),
+            CalculateVisibleRegion(),
+            ViewportRenderOutput, Scene.CalculateSampling(), Dimensions, CalculateResolution(), GuidValue, Delayed,
+            true, ForceRefreshFinalImage);
     }
 
     private void Image_MouseDown(object? sender, PointerPressedEventArgs e)
@@ -611,6 +626,32 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         scene.CenterContent(Document.GetRenderOutputSize(ViewportRenderOutput));
     }
 
+    private RectI? CalculateVisibleRegion()
+    {
+        if (Document is null) return null;
+        VecD viewportDimensions = scene.RealDimensions;
+        var transform = scene.CanvasTransform.Value.ToSKMatrix().ToMatrix3X3();
+        var docSize = Document.GetRenderOutputSize(ViewportRenderOutput);
+        if (docSize.X == 0 || docSize.Y == 0) return null;
+
+        VecD topLeft = new(0, 0);
+        VecD topRight = new(viewportDimensions.X, 0);
+        VecD bottomLeft = new(0, viewportDimensions.Y);
+        VecD bottomRight = new(viewportDimensions.X, viewportDimensions.Y);
+        topLeft = transform.Invert().MapPoint(topLeft);
+        topRight = transform.Invert().MapPoint(topRight);
+        bottomLeft = transform.Invert().MapPoint(bottomLeft);
+        bottomRight = transform.Invert().MapPoint(bottomRight);
+
+        double minX = Math.Min(Math.Min(topLeft.X, topRight.X), Math.Min(bottomLeft.X, bottomRight.X));
+        double maxX = Math.Max(Math.Max(topLeft.X, topRight.X), Math.Max(bottomLeft.X, bottomRight.X));
+        double minY = Math.Min(Math.Min(topLeft.Y, topRight.Y), Math.Min(bottomLeft.Y, bottomRight.Y));
+        double maxY = Math.Max(Math.Max(topLeft.Y, topRight.Y), Math.Max(bottomLeft.Y, bottomRight.Y));
+        RectD visibleRect = new(minX, minY, maxX - minX, maxY - minY);
+        visibleRect = visibleRect.Intersect(new RectD(0, 0, docSize.X, docSize.Y));
+        return (RectI)visibleRect.RoundOutwards();
+    }
+
     private static void CenterViewportTriggerChanged(AvaloniaPropertyChangedEventArgs<ExecutionTrigger<VecI>> e)
     {
         Viewport? viewport = (Viewport)e.Sender;
@@ -640,7 +681,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     private static void OnHighResPreviewChanged(AvaloniaPropertyChangedEventArgs<bool> e)
     {
         Viewport? viewport = (Viewport)e.Sender;
-        viewport.ForceRefreshFinalImage();
+        viewport.Document?.Operations.AddOrUpdateViewport(viewport.GetLocation());
     }
 
     private void MenuItem_OnClick(object? sender, PointerReleasedEventArgs e)

+ 4 - 29
src/PixiEditor/Views/Nodes/NodeView.cs

@@ -17,6 +17,7 @@ using Drawie.Backend.Core;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
+using PixiEditor.ViewModels.Document;
 using PixiEditor.Views.Nodes.Properties;
 
 namespace PixiEditor.Views.Nodes;
@@ -41,8 +42,8 @@ public class NodeView : TemplatedControl
         AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
             nameof(Outputs));
 
-    public static readonly StyledProperty<PreviewPainter> ResultPreviewProperty =
-        AvaloniaProperty.Register<NodeView, PreviewPainter>(
+    public static readonly StyledProperty<TexturePreview> ResultPreviewProperty =
+        AvaloniaProperty.Register<NodeView, TexturePreview>(
             nameof(ResultPreview));
 
     public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<NodeView, bool>(
@@ -94,7 +95,7 @@ public class NodeView : TemplatedControl
         set => SetValue(IsSelectedProperty, value);
     }
 
-    public PreviewPainter ResultPreview
+    public TexturePreview ResultPreview
     {
         get => GetValue(ResultPreviewProperty);
         set => SetValue(ResultPreviewProperty, value);
@@ -177,7 +178,6 @@ public class NodeView : TemplatedControl
     static NodeView()
     {
         IsSelectedProperty.Changed.Subscribe(NodeSelectionChanged);
-        ResultPreviewProperty.Changed.Subscribe(PainterChanged);
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -311,29 +311,4 @@ public class NodeView : TemplatedControl
             nodeView.PseudoClasses.Set(":selected", e.NewValue.Value);
         }
     }
-
-    private static void PainterChanged(AvaloniaPropertyChangedEventArgs<PreviewPainter> e)
-    {
-        if (e.Sender is NodeView nodeView)
-        {
-            if (e.OldValue.Value is not null)
-            {
-                e.OldValue.Value.CanRenderChanged -= nodeView.ResultPreview_CanRenderChanged;
-            }
-
-            if (e.NewValue.Value is not null)
-            {
-                e.NewValue.Value.CanRenderChanged += nodeView.ResultPreview_CanRenderChanged;
-                nodeView.ResultPreview_CanRenderChanged(e.NewValue.Value.CanRender);
-            }
-        }
-    }
-
-    private void ResultPreview_CanRenderChanged(bool canRender)
-    {
-        if (CanRenderPreview != canRender)
-        {
-            CanRenderPreview = canRender;
-        }
-    }
 }

+ 78 - 15
src/PixiEditor/Views/Rendering/Scene.cs

@@ -157,6 +157,15 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(MaxBilinearSamplingSizeProperty, value);
     }
 
+    public static readonly StyledProperty<Guid> ViewportIdProperty = AvaloniaProperty.Register<Scene, Guid>(
+        nameof(ViewportId));
+
+    public Guid ViewportId
+    {
+        get => GetValue(ViewportIdProperty);
+        set => SetValue(ViewportIdProperty, value);
+    }
+
     private Overlay? capturedOverlay;
 
     private List<Overlay> mouseOverOverlays = new();
@@ -239,8 +248,11 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         };
     }
 
-    private SamplingOptions CalculateSampling()
+    internal SamplingOptions CalculateSampling()
     {
+        if (Document == null)
+            return SamplingOptions.Default;
+
         if (Document.SizeBindable.LongestAxis > MaxBilinearSamplingSize)
         {
             return SamplingOptions.Default;
@@ -261,6 +273,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     protected override async void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         framebuffer?.Dispose();
         framebuffer = null;
 
@@ -311,46 +324,85 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         QueueNextFrame();
     }
 
-    public void Draw(DrawingSurface renderTexture)
+    public void Draw(DrawingSurface texture)
     {
         if (Document == null || SceneRenderer == null) return;
 
-        renderTexture.Canvas.Save();
+        texture.Canvas.Save();
         var matrix = CalculateTransformMatrix();
 
-        renderTexture.Canvas.SetMatrix(matrix.ToSKMatrix().ToMatrix3X3());
+        texture.Canvas.SetMatrix(matrix.ToSKMatrix().ToMatrix3X3());
 
         VecI outputSize = FindOutputSize();
 
         RectD dirtyBounds = new RectD(0, 0, outputSize.X, outputSize.Y);
-        RenderScene(dirtyBounds);
+        RenderScene(texture, dirtyBounds);
 
-        renderTexture.Canvas.Restore();
+        texture.Canvas.Restore();
     }
 
-    private void RenderScene(RectD bounds)
+    private void RenderScene(DrawingSurface texture, RectD bounds)
     {
         var renderOutput = RenderOutput == "DEFAULT" ? null : RenderOutput;
-        DrawCheckerboard(renderTexture.DrawingSurface, bounds);
-        DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Background);
+        DrawCheckerboard(texture, bounds);
+        DrawOverlays(texture, bounds, OverlayRenderSorting.Background);
         try
         {
-            SceneRenderer.RenderScene(renderTexture.DrawingSurface, CalculateResolution(), CalculateSampling(),
-                renderOutput);
+            if(Document == null || Document.SceneTextures.TryGetValue(ViewportId, out var tex) == false)
+                return;
+            
+            bool hasSaved = false;
+            int saved = -1;
+
+            var matrix = CalculateTransformMatrix().ToSKMatrix().ToMatrix3X3();
+            if(!Document.SceneTextures.TryGetValue(ViewportId, out var cachedTexture))
+                return;
+
+            Matrix3X3 matrixDiff = SolveMatrixDiff(matrix, cachedTexture);
+            var target = cachedTexture.DrawingSurface;
+
+            if (tex.Size == (VecI)RealDimensions || tex.Size == (VecI)(RealDimensions * SceneRenderer.OversizeFactor))
+            {
+                saved = texture.Canvas.Save();
+                texture.Canvas.ClipRect(bounds);
+                texture.Canvas.SetMatrix(matrixDiff);
+                hasSaved = true;
+            }
+            else
+            {
+                saved = texture.Canvas.Save();
+                ChunkResolution renderedResolution = ChunkResolution.Full;
+                if (SceneRenderer != null && SceneRenderer.LastRenderedStates.ContainsKey(ViewportId))
+                {
+                    renderedResolution = SceneRenderer.LastRenderedStates[ViewportId].ChunkResolution;
+                }
+                texture.Canvas.SetMatrix(matrixDiff);
+                texture.Canvas.Scale((float)renderedResolution.InvertedMultiplier());
+                hasSaved = true;
+            }
+
+
+            texture.Canvas.Save();
+
+            texture.Canvas.DrawSurface(target, 0, 0);
+            if (hasSaved)
+            {
+                texture.Canvas.RestoreToCount(saved);
+            }
         }
         catch (Exception e)
         {
-            renderTexture.DrawingSurface.Canvas.Clear();
+            texture.Canvas.Clear();
             using Paint paint = new Paint { Color = Colors.White, IsAntiAliased = true };
 
             using Font defaultSizedFont = Font.CreateDefault();
             defaultSizedFont.Size = 24;
 
-            renderTexture.DrawingSurface.Canvas.DrawText(new LocalizedString("ERROR_GRAPH"), renderTexture.Size / 2f,
+            texture.Canvas.DrawText(new LocalizedString("ERROR_GRAPH"), renderTexture.Size / 2f,
                 TextAlign.Center, defaultSizedFont, paint);
         }
 
-        DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Foreground);
+        DrawOverlays(texture, bounds, OverlayRenderSorting.Foreground);
     }
 
     private void DrawCheckerboard(DrawingSurface surface, RectD dirtyBounds)
@@ -734,7 +786,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         return new VecD(transformed.X, transformed.Y);
     }
 
-    private Matrix CalculateTransformMatrix()
+    internal Matrix CalculateTransformMatrix()
     {
         Matrix transform = Matrix.Identity;
         transform = transform.Append(Matrix.CreateRotation((float)AngleRadians));
@@ -844,6 +896,16 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         base.OnPropertyChanged(change);
     }
 
+
+    private Matrix3X3 SolveMatrixDiff(Matrix3X3 matrix, Texture cachedTexture)
+    {
+        Matrix3X3 old = cachedTexture.DrawingSurface.Canvas.TotalMatrix;
+        Matrix3X3 current = matrix;
+
+        Matrix3X3 solveMatrixDiff = current.Concat(old.Invert());
+        return solveMatrixDiff;
+    }
+
     private async Task<(bool success, string info)> DoInitialize(Compositor compositor,
         CompositionDrawingSurface surface)
     {
@@ -885,6 +947,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     protected async Task FreeGraphicsResources()
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         renderTexture?.Dispose();
         renderTexture = null;
 

+ 0 - 227
src/PixiEditor/Views/Visuals/PreviewPainterControl.cs

@@ -1,227 +0,0 @@
-using Avalonia;
-using Avalonia.Interactivity;
-using Avalonia.LogicalTree;
-using ChunkyImageLib.DataHolders;
-using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces;
-using Drawie.Interop.Avalonia.Core.Controls;
-using PixiEditor.Models.Rendering;
-using Drawie.Numerics;
-
-namespace PixiEditor.Views.Visuals;
-
-public class PreviewPainterControl : DrawieControl
-{
-    public static readonly StyledProperty<int> FrameToRenderProperty =
-        AvaloniaProperty.Register<PreviewPainterControl, int>("FrameToRender");
-
-    public static readonly StyledProperty<PreviewPainter> PreviewPainterProperty =
-        AvaloniaProperty.Register<PreviewPainterControl, PreviewPainter>(
-            nameof(PreviewPainter));
-
-    public static readonly StyledProperty<VecI> CustomRenderSizeProperty =
-        AvaloniaProperty.Register<PreviewPainterControl, VecI>(
-            nameof(CustomRenderSize));
-
-    public VecI CustomRenderSize
-    {
-        get => GetValue(CustomRenderSizeProperty);
-        set => SetValue(CustomRenderSizeProperty, value);
-    }
-
-    public PreviewPainter PreviewPainter
-    {
-        get => GetValue(PreviewPainterProperty);
-        set => SetValue(PreviewPainterProperty, value);
-    }
-
-    public int FrameToRender
-    {
-        get { return (int)GetValue(FrameToRenderProperty); }
-        set { SetValue(FrameToRenderProperty, value); }
-    }
-
-    private PainterInstance? painterInstance;
-
-    static PreviewPainterControl()
-    {
-        PreviewPainterProperty.Changed.Subscribe(PainterChanged);
-        BoundsProperty.Changed.Subscribe(UpdatePainterBounds);
-        CustomRenderSizeProperty.Changed.Subscribe(UpdatePainterBounds);
-    }
-
-    public PreviewPainterControl()
-    {
-    }
-
-    public PreviewPainterControl(PreviewPainter previewPainter, int frameToRender)
-    {
-        PreviewPainter = previewPainter;
-        FrameToRender = frameToRender;
-    }
-
-    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
-    {
-        base.OnDetachedFromVisualTree(e);
-        if (PreviewPainter != null && painterInstance != null)
-        {
-            PreviewPainter.RemovePainterInstance(painterInstance.RequestId);
-            painterInstance.RequestMatrix = null;
-            painterInstance.RequestRepaint = null;
-            painterInstance.RequestRenderBounds = null;
-            painterInstance = null;
-        }
-    }
-
-    protected override void OnLoaded(RoutedEventArgs e)
-    {
-        base.OnLoaded(e);
-        if (PreviewPainter != null && painterInstance == null)
-        {
-            painterInstance = PreviewPainter.AttachPainterInstance();
-            VecI finalSize = GetFinalSize();
-            if (finalSize is { X: > 0, Y: > 0 })
-            {
-                PreviewPainter.ChangeRenderTextureSize(painterInstance.RequestId, finalSize);
-            }
-
-            painterInstance.RequestMatrix = OnPainterRequestMatrix;
-            painterInstance.RequestRepaint = OnPainterRenderRequest;
-            painterInstance.RequestRenderBounds = OnPainterRequestBounds;
-
-            PreviewPainter.RepaintFor(painterInstance.RequestId);
-        }
-    }
-
-    private static void PainterChanged(AvaloniaPropertyChangedEventArgs<PreviewPainter> args)
-    {
-        var sender = args.Sender as PreviewPainterControl;
-        if (args.OldValue.Value != null)
-        {
-            if (sender.painterInstance != null)
-            {
-                args.OldValue.Value.RemovePainterInstance(sender.painterInstance.RequestId);
-            }
-
-
-            sender.painterInstance = null;
-        }
-
-        if (args.NewValue.Value != null)
-        {
-            sender.painterInstance = args.NewValue.Value.AttachPainterInstance();
-            VecI finalSize = sender.GetFinalSize();
-            if (finalSize is { X: > 0, Y: > 0 })
-            {
-                sender.PreviewPainter.ChangeRenderTextureSize(sender.painterInstance.RequestId, finalSize);
-            }
-
-            sender.painterInstance.RequestMatrix = sender.OnPainterRequestMatrix;
-            sender.painterInstance.RequestRepaint = sender.OnPainterRenderRequest;
-            sender.painterInstance.RequestRenderBounds = sender.OnPainterRequestBounds;
-
-            args.NewValue.Value.RepaintFor(sender.painterInstance.RequestId);
-        }
-        else
-        {
-            sender.painterInstance = null;
-        }
-    }
-
-    private void OnPainterRenderRequest()
-    {
-        QueueNextFrame();
-    }
-
-    public override void Draw(DrawingSurface surface)
-    {
-        if (PreviewPainter == null || painterInstance == null)
-        {
-            return;
-        }
-
-        if (CustomRenderSize.ShortestAxis > 0)
-        {
-            surface.Canvas.Save();
-            VecI finalSize = GetFinalSize();
-            surface.Canvas.Scale(
-                (float)Bounds.Width / finalSize.X,
-                (float)Bounds.Height / finalSize.Y);
-        }
-
-        PreviewPainter.Paint(surface, painterInstance.RequestId);
-
-        if (CustomRenderSize.ShortestAxis > 0)
-        {
-            surface.Canvas.Restore();
-        }
-    }
-
-    private Matrix3X3 UniformScale(float x, float y, RectD previewBounds)
-    {
-        VecI finalSize = GetFinalSize();
-        float scaleX = finalSize.X / x;
-        float scaleY = finalSize.Y / y;
-        var scale = Math.Min(scaleX, scaleY);
-        float dX = (float)finalSize.X / 2 / scale - x / 2;
-        dX -= (float)previewBounds.X;
-        float dY = (float)finalSize.Y / 2 / scale - y / 2;
-        dY -= (float)previewBounds.Y;
-        Matrix3X3 matrix = Matrix3X3.CreateScale(scale, scale);
-        return matrix.Concat(Matrix3X3.CreateTranslation(dX, dY));
-    }
-
-    private VecI GetFinalSize()
-    {
-        VecI finalSize = CustomRenderSize.ShortestAxis > 0
-            ? CustomRenderSize
-            : new VecI((int)Bounds.Width, (int)Bounds.Height);
-        if (Bounds.Width < finalSize.X && Bounds.Height < finalSize.Y)
-        {
-            finalSize = new VecI((int)Bounds.Width, (int)Bounds.Height);
-        }
-
-        return finalSize;
-    }
-
-    private static void UpdatePainterBounds(AvaloniaPropertyChangedEventArgs args)
-    {
-        var sender = args.Sender as PreviewPainterControl;
-
-        if (sender?.PreviewPainter == null)
-        {
-            return;
-        }
-
-        if (sender.painterInstance != null)
-        {
-            VecI finalSize = sender.GetFinalSize();
-            if (finalSize is { X: > 0, Y: > 0 })
-            {
-                sender.PreviewPainter.ChangeRenderTextureSize(sender.painterInstance.RequestId, finalSize);
-                sender.PreviewPainter.RepaintFor(sender.painterInstance.RequestId);
-            }
-        }
-    }
-
-    private Matrix3X3? OnPainterRequestMatrix()
-    {
-        RectD? previewBounds =
-            PreviewPainter?.PreviewRenderable?.GetPreviewBounds(FrameToRender, PreviewPainter.ElementToRenderName);
-
-        if (previewBounds == null || previewBounds.Value.IsZeroOrNegativeArea)
-        {
-            return null;
-        }
-
-        float x = (float)(previewBounds?.Width ?? 0);
-        float y = (float)(previewBounds?.Height ?? 0);
-
-        return UniformScale(x, y, previewBounds.Value);
-    }
-
-    private VecI OnPainterRequestBounds()
-    {
-        return GetFinalSize();
-    }
-}

+ 81 - 0
src/PixiEditor/Views/Visuals/PreviewTextureControl.cs

@@ -0,0 +1,81 @@
+using Avalonia;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Interop.Avalonia.Core.Controls;
+using Drawie.Numerics;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Views.Visuals;
+
+public class PreviewTextureControl : DrawieControl
+{
+    public static readonly StyledProperty<TexturePreview?> TexturePreviewProperty =
+        AvaloniaProperty.Register<PreviewTextureControl, TexturePreview?>(
+            nameof(TexturePreview));
+
+    public TexturePreview? TexturePreview
+    {
+        get => GetValue(TexturePreviewProperty);
+        set => SetValue(TexturePreviewProperty, value);
+    }
+
+    static PreviewTextureControl()
+    {
+        AffectsRender<PreviewTextureControl>(TexturePreviewProperty);
+        TexturePreviewProperty.Changed.Subscribe(OnTexturePreviewChanged);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        if (TexturePreview != null)
+        {
+            TexturePreview.Attach(this, GetBounds);
+            TexturePreview.TextureUpdated += QueueNextFrame;
+        }
+    }
+
+    private VecI GetBounds()
+    {
+        double width = double.IsPositive(Width) ? Width : Bounds.Width;
+        double height = double.IsPositive(Height) ? Height : Bounds.Height;
+        if (double.IsNaN(width) || double.IsInfinity(width))
+            width = Bounds.Width;
+        if (double.IsNaN(height) || double.IsInfinity(height))
+            height = Bounds.Height;
+
+        return new VecI((int)Math.Ceiling(width), (int)Math.Ceiling(height));
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        TexturePreview?.Detach(this);
+        if (TexturePreview != null)
+            TexturePreview.TextureUpdated -= QueueNextFrame;
+    }
+
+    public override void Draw(DrawingSurface surface)
+    {
+        if (TexturePreview is { Preview: not null } && TexturePreview.Preview is { IsDisposed: false })
+        {
+            VecD scaling = new(Bounds.Size.Width / TexturePreview.Preview.Size.X, Bounds.Size.Height / TexturePreview.Preview.Size.Y);
+            surface.Canvas.Save();
+            surface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            surface.Canvas.DrawSurface(TexturePreview.Preview.DrawingSurface, 0, 0);
+            surface.Canvas.Restore();
+        }
+    }
+
+    private static void OnTexturePreviewChanged(AvaloniaPropertyChangedEventArgs<TexturePreview?> args)
+    {
+        if (args.Sender is PreviewTextureControl control)
+        {
+            args.OldValue.Value?.Detach(control);
+            if(args.OldValue.Value != null)
+                args.OldValue.Value.TextureUpdated -= control.QueueNextFrame;
+            args.NewValue.Value?.Attach(control, () => control.GetBounds());
+            if(args.NewValue.Value != null)
+                args.NewValue.Value.TextureUpdated += control.QueueNextFrame;
+        }
+    }
+}

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit d7a83f53f4a0e6a0e0d011cb045ab1f2075e759b
+Subproject commit 0e9c1ec319cb59ab5dffe2eb0e7b8e4165b732d6

+ 18 - 2
tests/PixiEditor.Tests/BlendingTests.cs

@@ -1,9 +1,12 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using Drawie.Skia;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -39,13 +42,26 @@ public class BlendingTests : PixiEditorTest
         NodeGraph graph = new NodeGraph();
         var firstImageLayer = new ImageLayerNode(new VecI(1, 1), ColorSpace.CreateSrgbLinear());
 
+        using Paint redPaint = new Paint
+        {
+            Color = new Color(255, 0, 0, 255),
+            BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src
+        };
+
         var firstImg = firstImageLayer.GetLayerImageAtFrame(0);
-        firstImg.EnqueueDrawPixel(VecI.Zero, new Color(255, 0, 0, 255), Drawie.Backend.Core.Surfaces.BlendMode.Src);
+        firstImg.EnqueueDrawPaint(redPaint);
         firstImg.CommitChanges();
 
         var secondImageLayer = new ImageLayerNode(new VecI(1, 1), ColorSpace.CreateSrgbLinear());
         var secondImg = secondImageLayer.GetLayerImageAtFrame(0);
-        secondImg.EnqueueDrawPixel(VecI.Zero, new Color(255, 255, 255, 255), Drawie.Backend.Core.Surfaces.BlendMode.Src);
+
+        using var whitePaint = new Paint
+        {
+            Color = new Color(255, 255, 255, 255),
+            BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src
+        };
+
+        secondImg.EnqueueDrawPaint(whitePaint);
         secondImg.CommitChanges();
 
         var outputNode = new OutputNode();

+ 49 - 4
tests/PixiEditor.Tests/PixiEditorTest.cs

@@ -1,5 +1,11 @@
+using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
+using Drawie.Interop.Avalonia.Core;
 using Drawie.Numerics;
+using Drawie.RenderApi;
+using Drawie.RenderApi.OpenGL;
+using Drawie.RenderApi.Vulkan;
+using Drawie.Silk;
 using Drawie.Skia;
 using Drawie.Windowing;
 using DrawiEngine;
@@ -28,7 +34,15 @@ public class PixiEditorTest
 
         try
         {
-            var engine = DesktopDrawingEngine.CreateDefaultDesktop();
+            IRenderApi renderApi = new VulkanRenderApi();
+
+            if (System.OperatingSystem.IsMacOS())
+            {
+                renderApi = new OpenGlRenderApi();
+            }
+
+            var engine = new DrawingEngine(renderApi, new GlfwWindowingPlatform(renderApi), new SkiaDrawingBackend(),
+                new TestsRenderingDispatcher());
             var app = new TestingApp();
             Console.WriteLine("Running DrawieEngine with configuration:");
             Console.WriteLine($"\t- RenderApi: {engine.RenderApi}");
@@ -47,7 +61,7 @@ public class PixiEditorTest
         catch (Exception ex)
         {
             if (!DrawingBackendApi.HasBackend)
-                DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), new DrawieRenderingDispatcher());
+                DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), new TestsRenderingDispatcher());
         }
     }
 }
@@ -92,7 +106,6 @@ public class FullPixiEditorTest : PixiEditorTest
             .AddExtensionServices(loader)
             .BuildServiceProvider();
 
-
         var vm = services.GetRequiredService<ViewModelMain>();
         vm.Setup(services);
     }
@@ -112,7 +125,7 @@ public class FullPixiEditorTest : PixiEditorTest
         }
 
         public IAdditionalContentProvider? AdditionalContentProvider { get; } = new NullAdditionalContentProvider();
-        public IIdentityProvider? IdentityProvider { get; } 
+        public IIdentityProvider? IdentityProvider { get; }
     }
 }
 
@@ -130,4 +143,36 @@ public class TestingApp : DrawieApp
     {
         window.IsVisible = false;
     }
+}
+
+class TestsRenderingDispatcher : IRenderingDispatcher
+{
+    public Action<Action> Invoke { get; } = action => action();
+
+    public Task<TResult> InvokeAsync<TResult>(Func<TResult> function)
+    {
+        return Task.FromResult(function());
+    }
+
+    public Task<TResult> InvokeInBackgroundAsync<TResult>(Func<TResult> function)
+    {
+        return Task.FromResult(function());
+    }
+
+    public Task InvokeInBackgroundAsync(Action function)
+    {
+        function();
+        return Task.CompletedTask;
+    }
+
+    public Task InvokeAsync(Action function)
+    {
+        function();
+        return Task.CompletedTask;
+    }
+
+    public IDisposable EnsureContext()
+    {
+        return new EmptyDisposable();
+    }
 }

+ 14 - 3
tests/PixiEditor.Tests/RenderTests.cs

@@ -1,12 +1,15 @@
 using Avalonia.Headless.XUnit;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
 using PixiEditor.Models.IO;
+using PixiEditor.Models.Position;
 using Xunit.Abstractions;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 
@@ -89,12 +92,20 @@ public class RenderTests : FullPixiEditorTest
         string pixiFile = Path.Combine("TestFiles", "ResolutionTests", pixiName + ".pixi");
 
         var document = Importer.ImportDocument(pixiFile);
-        using Surface output = Surface.ForDisplay(document.SizeBindable);
-        document.SceneRenderer.RenderScene(output.DrawingSurface, ChunkResolution.Half, SamplingOptions.Default);
+        ViewportInfo info = new ViewportInfo(
+            0,
+            document.SizeBindable / 2f,
+            document.SizeBindable,
+            Matrix3X3.Identity, null, "DEFAULT", SamplingOptions.Default, document.SizeBindable, ChunkResolution.Half,
+            Guid.NewGuid(), false, false, () => { });
+        using var output = document.SceneRenderer.RenderScene(info, new AffectedArea(), null);
 
         Color expectedColor = Colors.Yellow;
 
-        Assert.True(AllPixelsAreColor(output, expectedColor));
+        using Surface surface = Surface.ForDisplay(document.SizeBindable);
+        surface.DrawingSurface.Canvas.DrawSurface(output.DrawingSurface, 0, 0);
+
+        Assert.True(AllPixelsAreColor(surface, expectedColor));
     }
 
     private static bool PixelCompare(Surface image, Surface compareTo)

BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/BlendingLinearSrgb.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/VectorRectangleClippedToCircleMasked.png