浏览代码

Merge branch 'master' into pattern-node

Krzysztof Krysiński 2 周之前
父节点
当前提交
d92c8d9c04
共有 100 个文件被更改,包括 2020 次插入1622 次删除
  1. 二进制
      assets/flatpak/icon-512.png
  2. 14 0
      assets/flatpak/net.pixieditor.PixiEditor-mime.xml
  3. 13 0
      assets/flatpak/net.pixieditor.PixiEditor.desktop
  4. 87 0
      assets/flatpak/net.pixieditor.PixiEditor.metainfo.xml
  5. 二进制
      assets/flatpak/screenshots/anim.png
  6. 二进制
      assets/flatpak/screenshots/graph.png
  7. 二进制
      assets/flatpak/screenshots/palettes.png
  8. 二进制
      assets/flatpak/screenshots/vector.png
  9. 19 8
      src/ChunkyImageLib/Chunk.cs
  10. 98 6
      src/ChunkyImageLib/ChunkyImage.cs
  11. 88 9
      src/ChunkyImageLib/ChunkyImageEx.cs
  12. 1 1
      src/ChunkyImageLib/CommittedChunkStorage.cs
  13. 27 25
      src/ChunkyImageLib/DataHolders/ColorBounds.cs
  14. 1 0
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  15. 1 1
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  16. 7 3
      src/ChunkyImageLib/Operations/ImageOperation.cs
  17. 8 14
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  18. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Filter.cs
  19. 0 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyLayerNode.cs
  21. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNodeGraph.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  23. 12 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  24. 78 21
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  25. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CacheTriggerFlags.cs
  26. 11 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  27. 2 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  28. 29 35
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  29. 3 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs
  30. 0 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs
  31. 9 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  32. 20 21
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  33. 65 83
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  34. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  35. 15 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  36. 4 31
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  37. 25 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  38. 7 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  39. 12 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  40. 3 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  41. 19 24
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  42. 57 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  43. 4 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs
  44. 8 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  45. 59 75
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  46. 0 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  47. 25 17
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  48. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs
  49. 28 11
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  50. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  51. 4 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  52. 6 4
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs
  53. 2 1
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  54. 0 17
      src/PixiEditor.ChangeableDocument/Helpers/PreviewUtils.cs
  55. 1 118
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  56. 43 0
      src/PixiEditor.ChangeableDocument/Rendering/PreviewRenderRequest.cs
  57. 48 0
      src/PixiEditor.ChangeableDocument/Rendering/PreviewUtility.cs
  58. 19 3
      src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs
  59. 6 12
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  60. 18 3
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  61. 27 0
      src/PixiEditor/Helpers/Converters/ResultPreviewIsPresentConverter.cs
  62. 35 25
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  63. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  64. 7 0
      src/PixiEditor/Models/DocumentPassthroughActions/RefreshPreview_PassthroughAction.cs
  65. 2 1
      src/PixiEditor/Models/Handlers/ICelHandler.cs
  66. 2 2
      src/PixiEditor/Models/Handlers/IDocument.cs
  67. 3 7
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  68. 3 2
      src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs
  69. 1 0
      src/PixiEditor/Models/Handlers/Tools/IMoveToolHandler.cs
  70. 9 1
      src/PixiEditor/Models/Position/ViewportInfo.cs
  71. 40 2
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  72. 3 3
      src/PixiEditor/Models/Rendering/AnimationPreviewRenderer.cs
  73. 0 210
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  74. 272 103
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  75. 0 284
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  76. 240 148
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  77. 5 0
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  78. 7 0
      src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs
  79. 61 18
      src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs
  80. 15 4
      src/PixiEditor/Models/Serialization/Factories/SurfaceSerializationFactory.cs
  81. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  82. 3 3
      src/PixiEditor/Styles/Templates/KeyFrame.axaml
  83. 1 1
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  84. 18 8
      src/PixiEditor/Styles/Templates/NodeView.axaml
  85. 2 2
      src/PixiEditor/Styles/Templates/TimelineGroupHeader.axaml
  86. 8 8
      src/PixiEditor/ViewModels/Document/CelViewModel.cs
  87. 5 23
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  88. 15 24
      src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs
  89. 75 0
      src/PixiEditor/ViewModels/Document/TexturePreview.cs
  90. 6 8
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  91. 25 10
      src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs
  92. 7 0
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  93. 2 0
      src/PixiEditor/ViewModels/ViewModelMain.cs
  94. 1 0
      src/PixiEditor/Views/Dock/DocumentPreviewDockView.axaml
  95. 4 6
      src/PixiEditor/Views/Layers/FolderControl.axaml
  96. 5 7
      src/PixiEditor/Views/Layers/LayerControl.axaml
  97. 1 0
      src/PixiEditor/Views/Main/DocumentPreview.axaml
  98. 8 0
      src/PixiEditor/Views/Main/DocumentPreview.axaml.cs
  99. 10 15
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml
  100. 58 14
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs

二进制
assets/flatpak/icon-512.png


+ 14 - 0
assets/flatpak/net.pixieditor.PixiEditor-mime.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+
+  <!-- Native Pixi format -->
+  <mime-type type="application/x-pixi">
+    <comment>Pixi Project File</comment>
+    <generic-icon name="application-x-pixi"/>
+    <glob pattern="*.pixi"/>
+  </mime-type>
+</mime-info>
+

+ 13 - 0
assets/flatpak/net.pixieditor.PixiEditor.desktop

@@ -0,0 +1,13 @@
+[Desktop Entry]
+Name=PixiEditor
+Comment=PixiEditor is all-in-one solution for 2D image editing.
+Icon=net.pixieditor.PixiEditor
+Exec=pixieditor.sh %u
+StartupWMClass=pixieditor
+Terminal=false
+Type=Application
+Categories=Graphics;2DGraphics;RasterGraphics;VectorGraphics
+MimeType=application/x-pixi;image/jpeg;image/png;image/gif;image/bmp;image/webp;image/svg+xml;font/otf;font/ttf;x-scheme-handler/lospec-palette;
+GenericName=2D Editor
+SingleMainWindow=true
+Keywords=editor;image;2d;graphics;design;vector;raster;

+ 87 - 0
assets/flatpak/net.pixieditor.PixiEditor.metainfo.xml

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2025 Krzysztof Krysiński -->
+<component type="desktop-application">
+  <id>net.pixieditor.PixiEditor</id>
+  
+  <name>PixiEditor</name>
+  <summary>Universal node-based 2D editor</summary>
+  
+  <metadata_license>CC-BY-SA-4.0</metadata_license>
+  <project_license>LGPL-3.0-or-later</project_license>
+
+  <developer id="net.pixieditor">
+    <name>PixiEditor</name>
+  </developer>
+
+  <requires>
+    <control>keyboard</control>
+    <control>pointing</control>
+    <display_length compare="ge">768</display_length>
+  </requires>
+
+  <url type="homepage">https://pixieditor.net</url>
+  <url type="bugtracker">https://github.com/PixiEditor/PixiEditor/issues</url>
+  <url type="donation">https://pixieditor.net/download</url>
+  <url type="contact">https://pixieditor.net/help</url>
+  <url type="faq">https://pixieditor.net/docs/faq</url>
+  <url type="contribute">https://github.com/PixiEditor/PixiEditor</url>
+  <url type="vcs-browser">https://github.com/PixiEditor/PixiEditor</url>
+
+  <icon type="stock">net.pixieditor.PixiEditor</icon>
+
+  <branding>
+    <color type="primary" scheme_preference="light">#dedede</color>
+    <color type="primary" scheme_preference="dark">#1a1a1a</color>
+  </branding>
+
+  <content_rating type="oars-1.1" />
+
+  <releases>
+    <release version="2.0.1.16" date="2025-10-06">
+      <url type="details">https://forum.pixieditor.net/t/changelog-2-0-1-16/461</url>
+      <description>
+        <p>Posterize and Text Nodes, transforming improvements and new languagesW</p>
+      </description>      
+    </release>
+    <release version="2.0.1.14" date="2025-09-10">
+      <url type="details">https://forum.pixieditor.net/t/changelog-2-0-1-14/439/1</url>
+      <description>
+        <p>Fixed rectangle rendering issues</p>
+      </description>      
+    </release>
+  </releases>
+  
+  <description>
+    <p>
+	PixiEditor is a universal 2D editor designed for all kinds of creative work. Whether you want to make game sprites, paint illustrations, design logos, edit images, or create animations, PixiEditor gives you the tools to bring your ideas to life—all in a clean and familiar interface.
+    </p>
+    <p>
+	Powered by a Node Graph for advanced, non-destructive editing, PixiEditor comes with three unique toolsets that can be used together on the same canvas:
+    </p>
+    <p>
+	Pixel Art – pixel-perfect drawing tools for sprites and retro graphics
+	Painting – soft brushes, smooth lines, and anti-aliased shapes
+		    Vector – scalable paths and shapes for logos and clean designs
+    </p>
+  </description>
+  
+  <launchable type="desktop-id">net.pixieditor.PixiEditor.desktop</launchable>
+  <screenshots>
+    <screenshot type="default">
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/anim.png</image>
+	    <caption>Frame-by-frame animation with onion skinning</caption>
+    </screenshot>
+    <screenshot>
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/graph.png</image>
+	    <caption>Procedurally generated islands</caption>
+    </screenshot>
+    <screenshot>
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/palettes.png</image>
+	    <caption>Palette browser</caption>
+    </screenshot>
+    <screenshot>
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/vector.png</image>
+	    <caption>Vectors editing tools</caption>
+    </screenshot>
+  </screenshots>
+</component>

二进制
assets/flatpak/screenshots/anim.png


二进制
assets/flatpak/screenshots/graph.png


二进制
assets/flatpak/screenshots/palettes.png


二进制
assets/flatpak/screenshots/vector.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;*/
     }
 }

+ 98 - 6
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)
     {
@@ -636,12 +701,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void EnqueueDrawImage(Matrix3X3 transformMatrix, Surface image, Paint? paint = null, bool copyImage = true)
+    public void EnqueueDrawImage(Matrix3X3 transformMatrix, Surface image, SamplingOptions samplingOptions, Paint? paint = null, bool copyImage = true)
     {
         lock (lockObject)
         {
             ThrowIfDisposed();
-            ImageOperation operation = new(transformMatrix, image, paint, copyImage);
+            ImageOperation operation = new(transformMatrix, image, samplingOptions, paint, copyImage);
             EnqueueOperation(operation);
         }
     }
@@ -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);
         }
     }
 

+ 27 - 25
src/ChunkyImageLib/DataHolders/ColorBounds.cs

@@ -21,27 +21,9 @@ public struct ColorBounds
 
     public float UpperA { get; set; }
 
-    public ColorBounds(Color color, double tolerance = 0)
+    public ColorBounds(ColorF color, double tolerance = 0)
     {
-        static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
-        {
-            float subHalf = channel > 0 ? channel - 1f : channel;
-            float addHalf = channel < 255 ? channel + 1f : channel;
-            
-            var lower = subHalf * alpha / 255f;
-            var upper = addHalf * alpha / 255f;
-            
-            return (lower, upper);
-        }
-
-        static (float lower, float upper) FindInclusiveBoundary(byte channel)
-        {
-            float subHalf = channel > 0 ? channel - .5f : channel;
-            float addHalf = channel < 255 ? channel + .5f : channel;
-            return (subHalf / 255f, addHalf / 255f);
-        }
-
-        float a = color.A / 255f;
+        float a = color.A;
 
         (LowerR, UpperR) = FindInclusiveBoundaryPremul(color.R, a);
         LowerR -= (float)tolerance;
@@ -60,6 +42,26 @@ public struct ColorBounds
         UpperA += (float)tolerance;
     }
 
+    private static (float lower, float upper) FindInclusiveBoundaryPremul(float channel, float alpha)
+    {
+        var step = 1f / 255f;
+        float subHalf = channel > 0 ? channel - step : channel;
+        float addHalf = channel < 1 ? channel + step : channel;
+
+        var lower = subHalf * alpha;
+        var upper = addHalf * alpha;
+
+        return (lower, upper);
+    }
+
+    private static (float lower, float upper) FindInclusiveBoundary(float channel)
+    {
+        float halfStep = 0.5f / 255f;
+        float subHalf = channel > 0 ? channel - halfStep : channel;
+        float addHalf = channel < 1 ? channel + halfStep : channel;
+        return (subHalf, addHalf);
+    }
+
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public unsafe bool IsWithinBounds(Half* pixel)
     {
@@ -78,12 +80,12 @@ public struct ColorBounds
         return true;
     }
 
-    public bool IsWithinBounds(Color toCompare)
+    public bool IsWithinBounds(ColorF toCompare)
     {
-        float a = toCompare.A / 255f;
-        float r = (toCompare.R / 255f) * a;
-        float g = (toCompare.G / 255f) * a;
-        float b = (toCompare.B / 255f) * a;
+        float a = toCompare.A;
+        float r = (toCompare.R) * a;
+        float g = (toCompare.G) * a;
+        float b = (toCompare.B) * a;
         
         if (r < LowerR || r > UpperR)
             return false;

+ 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/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -111,7 +111,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
             if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             {
-                RectD rect = (RectD)ellipseFillRect!.Value;
+                RectD rect = (((RectD?)(ellipseFillRect)) ?? (RectD?)location).Value;
                 fillPaintable.Bounds = location;
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 {

+ 7 - 3
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -1,6 +1,7 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -12,6 +13,7 @@ internal class ImageOperation : IMirroredDrawOperation
     private ShapeCorners corners;
     private Surface toPaint;
     private bool imageWasCopied = false;
+    private SamplingOptions samplingOptions = SamplingOptions.Default;
     private readonly Paint? customPaint;
 
     public bool IgnoreEmptyChunks => false;
@@ -31,6 +33,7 @@ internal class ImageOperation : IMirroredDrawOperation
         transformMatrix = Matrix3X3.CreateIdentity();
         transformMatrix.TransX = pos.X;
         transformMatrix.TransY = pos.Y;
+        this.samplingOptions = samplingOptions;
 
         // copying is needed for thread safety
         if (copyImage)
@@ -56,7 +59,7 @@ internal class ImageOperation : IMirroredDrawOperation
         imageWasCopied = copyImage;
     }
 
-    public ImageOperation(Matrix3X3 transformMatrix, Surface image, Paint? paint = null, bool copyImage = true)
+    public ImageOperation(Matrix3X3 transformMatrix, Surface image, SamplingOptions samplingOptions, Paint? paint = null, bool copyImage = true)
     {
         if (paint is not null)
             customPaint = paint.Clone();
@@ -69,6 +72,7 @@ internal class ImageOperation : IMirroredDrawOperation
             BottomRight = transformMatrix.MapPoint(image.Size),
         };
         this.transformMatrix = transformMatrix;
+        this.samplingOptions = samplingOptions;
 
         // copying is needed for thread safety
         if (copyImage)
@@ -100,12 +104,12 @@ internal class ImageOperation : IMirroredDrawOperation
             ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
             RectD rect = chunkCorners.WithMatrix(finalMatrix.Invert()).AABBBounds;
 
-            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint);
+            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint, samplingOptions);
         }
         else
         {
             // Slower, but works with perspective transformation
-            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, customPaint);
+            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, samplingOptions, customPaint);
         }
 
         targetChunk.Surface.DrawingSurface.Canvas.Restore();

+ 8 - 14
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -33,7 +33,10 @@ public class FFMpegRenderer : IAnimationRenderer
             MakeExecutableIfNeeded(binaryPath);
         }
 
-        string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
+        string tempPath = Path.Combine(Path.GetTempPath(), "PixiEditor", "Rendering");
+        Directory.CreateDirectory(tempPath);
+
+        string paletteTempPath = Path.Combine(tempPath, "palette.png");
 
         try
         {
@@ -46,12 +49,6 @@ public class FFMpegRenderer : IAnimationRenderer
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
-
-            if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
-            {
-                Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
-            }
-
             if (RequiresPaletteGeneration())
             {
                 GeneratePalette(streamPipeSource, paletteTempPath);
@@ -98,7 +95,10 @@ public class FFMpegRenderer : IAnimationRenderer
             MakeExecutableIfNeeded(binaryPath);
         }
 
-        string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
+        string tempPath = Path.Combine(Path.GetTempPath(), "PixiEditor", "Rendering");
+        Directory.CreateDirectory(tempPath);
+
+        string paletteTempPath = Path.Combine(tempPath, "palette.png");
 
         try
         {
@@ -111,12 +111,6 @@ public class FFMpegRenderer : IAnimationRenderer
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
-
-            if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
-            {
-                Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
-            }
-
             if (RequiresPaletteGeneration())
             {
                 GeneratePalette(streamPipeSource, paletteTempPath);

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

+ 29 - 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;
 
@@ -62,8 +66,8 @@ public class CreateImageNode : Node, IPreviewRenderable
             return null;
         }
 
-        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)
@@ -106,45 +110,35 @@ public class CreateImageNode : Node, IPreviewRenderable
         surface.Canvas.RestoreToCount(saved);
     }
 
-    public override Node CreateCopy() => new CreateImageNode();
-
-    public override void Dispose()
+    private void RenderPreviews(Texture surface, RenderContext context)
     {
-        base.Dispose();
-        textureCache.Dispose();
-    }
-
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        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;
-        }
-
-        if (Output.Value == null)
-        {
-            return false;
-        }
-
-        var surface = Render(context);
+            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 (surface == null || surface.IsDisposed)
-        {
-            return false;
+            texture.DrawingSurface.Canvas.Clear();
+            texture.DrawingSurface.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
         }
+    }
 
-        renderOn.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+    public override Node CreateCopy() => new CreateImageNode();
 
-        return true;
+    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";

+ 28 - 11
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,26 +54,34 @@ 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);
         VecI imageSizeInChunks = (VecI)(document.Size / (double)chunkSize).Ceiling();
         VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
         var chunkAtPos = cache.GetChunk(initChunkPos);
-        Color colorToReplace = chunkAtPos.Match(
-            (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
+        ColorF colorToReplace = chunkAtPos.Match(
+            (Chunk chunk) => chunk.Surface.GetRawPixelPrecise(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
 
         ulong uLongColor = drawingColor.ToULong();
-        Color colorSpaceCorrectedColor = drawingColor;
+        ColorF colorSpaceCorrectedColor = drawingColor;
         if (!document.ProcessingColorSpace.IsSrgb)
         {
-            var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
+            // Mixing using actual surfaces is more accurate than using ColorTransformFn
+            // mismatch between actual surface color and transformed color here can lead to infinite loops
+            using Surface srgbSurface = Surface.ForProcessing(new VecI(1), ColorSpace.CreateSrgb());
+            using Paint srgbPaint = new Paint(){ Color = drawingColor };
+            srgbSurface.DrawingSurface.Canvas.DrawPixel(0, 0, srgbPaint);
+            using var processingSurface = Surface.ForProcessing(VecI.One, document.ProcessingColorSpace);
+            processingSurface.DrawingSurface.Canvas.DrawSurface(srgbSurface.DrawingSurface, 0, 0);
+            var fixedColor = processingSurface.GetRawPixelPrecise(VecI.Zero);
 
-            var fixedColor = drawingColor.TransformColor(srgbTransform);
             uLongColor = fixedColor.ToULong();
-            colorSpaceCorrectedColor = (Color)fixedColor;
+            colorSpaceCorrectedColor = fixedColor;
         }
 
         if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
@@ -192,15 +202,16 @@ public static class FloodFillHelper
         VecI chunkPos,
         int chunkSize,
         ulong colorBits,
-        Color color,
+        ColorF color,
         VecI pos,
         ColorBounds bounds,
         bool checkFirstPixel)
     {
+        var rawPixelRef = referenceChunk.Surface.GetRawPixelPrecise(pos);
         // color should be a fixed color
-        if (referenceChunk.Surface.GetRawPixel(pos) == color || drawingChunk.Surface.GetRawPixel(pos) == color)
+        if ((Color)rawPixelRef == (Color)color || (Color)drawingChunk.Surface.GetRawPixelPrecise(pos) == (Color)color)
             return null;
-        if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
+        if (checkFirstPixel && !bounds.IsWithinBounds(rawPixelRef))
             return null;
         
         if(!SelectionIntersectsChunk(selection, chunkPos, chunkSize))
@@ -209,10 +220,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();
@@ -240,6 +253,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);

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -28,6 +28,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
     private VecD tightBoundsSize;
     private RectD cornersToSelectionOffset;
     private VecD originalCornersSize;
+    private bool bilinearFiltering;
 
     private bool isTransformingSelection;
     private bool hasEnqueudImages = false;
@@ -42,11 +43,13 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
     public TransformSelected_UpdateableChange(
         ShapeCorners masterCorners,
         bool keepOriginal,
+        bool bilinearFiltering,
         Dictionary<Guid, ShapeCorners> memberCorners,
         bool transformMask,
         int frame)
     {
         memberData = new();
+        this.bilinearFiltering = bilinearFiltering;
         foreach (var corners in memberCorners)
         {
             memberData.Add(new MemberTransformationData(corners.Key) { MemberCorners = corners.Value });
@@ -417,7 +420,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             finalPaint = LockedAlphaPaint;
         }
 
-        memberImage.EnqueueDrawImage(data.LocalMatrix, data.Image, finalPaint, false);
+        memberImage.EnqueueDrawImage(data.LocalMatrix, data.Image, bilinearFiltering ? SamplingOptions.Bilinear : SamplingOptions.Default, finalPaint, false);
         hasEnqueudImages = true;
 
         var affectedArea = memberImage.FindAffectedArea();

+ 6 - 4
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;
@@ -125,8 +126,8 @@ internal class MagicWandHelper
         VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
 
 
-        Color colorToReplace = cache.GetChunk(initChunkPos).Match(
-            (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
+        ColorF colorToReplace = cache.GetChunk(initChunkPos).Match(
+            (Chunk chunk) => chunk.Surface.GetRawPixelPrecise(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
 
@@ -257,14 +258,15 @@ internal class MagicWandHelper
         VecI pos,
         ColorBounds bounds, Lines lines)
     {
-        if (!bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+        if (!bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixelPrecise(pos)))
         {
             return null;
         }
 
         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
         };
     }
 }

+ 6 - 12
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -24,7 +24,7 @@
     <PropertyGroup>
       <ProtogenExists>false</ProtogenExists>
     </PropertyGroup>
-    <Exec Command="dotnet tool run protogen --version" IgnoreExitCode="true">
+    <Exec ContinueOnError="true" Command="dotnet tool run protogen --version" IgnoreExitCode="true">
       <Output TaskParameter="ExitCode" PropertyName="ProtogenExitCode"/>
     </Exec>
     <PropertyGroup>
@@ -32,21 +32,15 @@
     </PropertyGroup>
   </Target>
 
-  <Target Name="InstallProtogen" BeforeTargets="GenerateProtoContracts"
+  <Target Name="WarnProtogen" BeforeTargets="GenerateProtoContracts"
           Condition="'$(ProtogenExists)' != 'true'">
-    <Message Text="Downloading protogen v$(ProtogenVersion)..." Importance="high"/>
-    <Exec Command="dotnet tool install --local protobuf-net.Protogen --version $(ProtogenVersion)"/>
-    <PropertyGroup>
-      <ProtogenExists>true</ProtogenExists>
-    </PropertyGroup>
-
-    <Message Text="protogen installed successfully." Importance="high"/>
+    <Message Text="protogen is not installed. Skipping generating contracts" Importance="high"/>
   </Target>
 
-
   <Target Name="GenerateProtoContracts" BeforeTargets="BeforeCompile"
-          Inputs="$(MSBuildProjectDirectory)\DataContracts\*.proto"
-          Outputs="$(MSBuildProjectDirectory)\ProtoAutogen\*.cs">
+          Condition="'$(ProtogenExists)' == 'true'"
+    Inputs="$(MSBuildProjectDirectory)\DataContracts\*.proto"
+    Outputs="$(MSBuildProjectDirectory)\ProtoAutogen\*.cs">
     <Exec Command="dotnet tool run protogen --csharp_out=ProtoAutogen --proto_path=DataContracts +listset=yes *.proto"/>
 
     <ItemGroup>

+ 18 - 3
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -5,7 +5,12 @@
     "Tools": [
       "MoveViewport",
       "RotateViewport",
-      "Move",
+      {
+        "ToolName": "Move",
+        "Settings": {
+          "BilinearTransform": false
+        }
+      },
       {
         "ToolName": "Pen",
         "Settings": {
@@ -52,7 +57,12 @@
     "Tools": [
       "MoveViewport",
       "RotateViewport",
-      "Move",
+      {
+        "ToolName": "Move",
+        "Settings": {
+          "BilinearTransform": true
+        }
+      },
       {
         "ToolName": "Pen",
         "Settings": {
@@ -127,7 +137,12 @@
     "Tools": [
       "MoveViewport",
       "RotateViewport",
-      "Move",
+      {
+        "ToolName": "Move",
+        "Settings": {
+          "BilinearTransform": true
+        }
+      },
       "VectorPath",
       "VectorLine",
       "VectorEllipse",

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

+ 2 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -341,14 +341,14 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         if (!movedOnce)
         {
             internals!.ActionAccumulator.AddActions(
-                new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, memberCorners, false,
+                new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, tool.BilinearTransform, memberCorners, false,
                     document.AnimationHandler.ActiveFrameBindable));
 
             movedOnce = true;
         }
 
         internals!.ActionAccumulator.AddActions(
-            new TransformSelected_Action(corners, tool!.KeepOriginalImage, memberCorners, false,
+            new TransformSelected_Action(corners, tool!.KeepOriginalImage, tool.BilinearTransform, memberCorners, false,
                 document!.AnimationHandler.ActiveFrameBindable));
     }
 

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

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IMoveToolHandler.cs

@@ -7,4 +7,5 @@ internal interface IMoveToolHandler : IToolHandler
     public bool KeepOriginalImage { get; }
     public bool TransformingSelectedArea { get; set; }
     public bool DuplicateOnMove { get; set; }
+    public bool BilinearTransform { 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)

+ 7 - 0
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -49,6 +49,13 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         set => SetValue(value);
     }
 
+    [Settings.Bool("_bilinear_transform", false, ExposedByDefault = false)]
+    public bool BilinearTransform
+    {
+        get => GetValue<bool>();
+        set => SetValue(value);
+    }
+
     public override BrushShape FinalBrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;

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

部分文件因为文件数量过多而无法显示