Browse Source

Added caching to bunch of elements

flabbet 7 months ago
parent
commit
f9a6a7f795

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -17,7 +17,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         constantNonOverrideValue = defaultValue;
         NonOverridenValue = _ => constantNonOverrideValue;
     }
-
+    
     protected internal override object FuncFactory(object toReturn)
     {
         Func<FuncContext, T> func = _ =>

+ 32 - 16
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -12,7 +12,10 @@ public class InputProperty : IInputProperty
     private object _internalValue;
     private int _lastExecuteHash = -1;
     private PropertyValidator? validator;
-    
+    private IOutputProperty? connection;
+
+    public event Action ConnectionChanged;
+
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
 
@@ -26,7 +29,7 @@ public class InputProperty : IInputProperty
             }
 
             var connectionValue = Connection.Value;
-            
+
             if (!ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is Delegate connectionField)
             {
                 return connectionField.DynamicInvoke(FuncContext.NoContext);
@@ -40,7 +43,7 @@ public class InputProperty : IInputProperty
             return connectionValue;
         }
     }
-    
+
     public object NonOverridenValue
     {
         get => _internalValue;
@@ -49,7 +52,7 @@ public class InputProperty : IInputProperty
             _internalValue = value;
         }
     }
-    
+
     public PropertyValidator Validator
     {
         get
@@ -73,13 +76,16 @@ public class InputProperty : IInputProperty
     {
         Func<FuncContext, object> func = f =>
         {
-            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result) ? result : null;
+            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result)
+                ? result
+                : null;
         };
         return func;
     }
 
     public Node Node { get; }
-    public Type ValueType { get; } 
+    public Type ValueType { get; }
+
     internal bool CacheChanged
     {
         get
@@ -89,12 +95,12 @@ public class InputProperty : IInputProperty
                 return cacheable.GetCacheHash() != _lastExecuteHash;
             }
 
-            if(Value is null)
+            if (Value is null)
             {
                 return _lastExecuteHash != 0;
             }
-            
-            if(Value.GetType().IsValueType || Value.GetType() == typeof(string))
+
+            if (Value.GetType().IsValueType || Value.GetType() == typeof(string))
             {
                 return Value.GetHashCode() != _lastExecuteHash;
             }
@@ -118,11 +124,22 @@ public class InputProperty : IInputProperty
             _lastExecuteHash = Value.GetHashCode();
         }
     }
-    
+
     IReadOnlyNode INodeProperty.Node => Node;
-    
-    public IOutputProperty? Connection { get; set; }
-    
+
+    public IOutputProperty? Connection
+    {
+        get => connection;
+        set
+        {
+            if (connection != value)
+            {
+                connection = value;
+                ConnectionChanged?.Invoke();
+            }
+        }
+    }
+
     internal InputProperty(Node node, string internalName, string displayName, object defaultValue, Type valueType)
     {
         InternalPropertyName = internalName;
@@ -133,7 +150,6 @@ public class InputProperty : IInputProperty
     }
 }
 
-
 public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
     public new T Value
@@ -150,9 +166,9 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             {
                 return (T)FuncFactoryDelegate(func);
             }
-            
+
             object target = value;
-            if(value is ShaderExpressionVariable shaderExpression)
+            if (value is ShaderExpressionVariable shaderExpression)
             {
                 target = shaderExpression.GetConstant();
             }

+ 23 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -1,14 +1,14 @@
-using System.Collections;
-using System.Diagnostics;
+using System.Collections.Immutable;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 {
+    private ImmutableList<IReadOnlyNode>? cachedExecutionList;
+    
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
@@ -23,8 +23,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         {
             return;
         }
-
+        
+        node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
+        ResetCache();
     }
 
     public void RemoveNode(Node node)
@@ -34,12 +36,19 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
             return;
         }
 
+        node.ConnectionsChanged -= ResetCache;
         _nodes.Remove(node);
+        ResetCache();
     }
 
     public Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode outputNode)
     {
-        return GraphUtils.CalculateExecutionQueue(outputNode);
+        return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
+    }
+    
+    private ImmutableList<IReadOnlyNode> CalculateExecutionQueueInternal(IReadOnlyNode outputNode)
+    {
+        return cachedExecutionList ??= GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
     }
 
     void IReadOnlyNodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
@@ -58,11 +67,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
         if(OutputNode == null) return false;
         
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueueInternal(OutputNode);
         
-        while (queue.Count > 0)
+        foreach (var node in queue)
         {
-            var node = queue.Dequeue();
             action(node);
         }
         
@@ -74,12 +82,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         if (OutputNode == null) return;
         if(!CanExecute()) return;
 
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueueInternal(OutputNode);
         
-        while (queue.Count > 0)
+        foreach (var node in queue)
         {
-            var node = queue.Dequeue();
-            
             if (node is Node typedNode)
             {
                 if(typedNode.IsDisposed) continue;
@@ -105,4 +111,9 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
         return true;
     }
+    
+    private void ResetCache()
+    {
+        cachedExecutionList = null;
+    }
 }

+ 9 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -21,6 +21,8 @@ public class CreateImageNode : Node
 
     public RenderOutputProperty RenderOutput { get; }
 
+    private TextureCache textureCache = new();
+    
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
@@ -37,7 +39,7 @@ public class CreateImageNode : Node
             return;
         }
 
-        var surface = RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
+        var surface = textureCache.RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
 
         surface.DrawingSurface.Canvas.Clear(Fill.Value);
 
@@ -61,4 +63,10 @@ public class CreateImageNode : Node
     }
 
     public override Node CreateCopy() => new CreateImageNode();
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose();
+    }
 }

+ 0 - 57
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs

@@ -1,57 +0,0 @@
-using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
-using DrawingApiBlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
-using Drawie.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-// TODO: Add based on debug mode, not debug build.
-[NodeInfo("DebugBlendMode")]
-public class DebugBlendModeNode : Node
-{
-    private Paint _paint = new();
-    
-    public InputProperty<Texture?> Dst { get; }
-
-    public InputProperty<Texture?> Src { get; }
-
-    public InputProperty<DrawingApiBlendMode> BlendMode { get; }
-
-    public OutputProperty<Texture> Result { get; }
-
-    private Paint blendModeOpacityPaint => new() { BlendMode = DrawingApiBlendMode.SrcOver }; 
-    public DebugBlendModeNode()
-    {
-        Dst = CreateInput<Texture?>(nameof(Dst), "Dst", null);
-        Src = CreateInput<Texture?>(nameof(Src), "Src", null);
-        BlendMode = CreateInput(nameof(BlendMode), "Blend Mode", DrawingApiBlendMode.SrcOver);
-
-        Result = CreateOutput<Texture>(nameof(Result), "Result", null);
-    }
-
-    protected override void OnExecute(RenderContext context)
-    {
-        if (Dst.Value is not { } dst || Src.Value is not { } src)
-            return;
-
-        var size = new VecI(Math.Max(src.Size.X, dst.Size.X), int.Max(src.Size.Y, dst.Size.Y));
-        var workingSurface = RequestTexture(0, size, context.ProcessingColorSpace);
-
-        workingSurface.DrawingSurface.Canvas.DrawSurface(dst.DrawingSurface, 0, 0, blendModeOpacityPaint);
-
-        _paint.BlendMode = BlendMode.Value;
-        workingSurface.DrawingSurface.Canvas.DrawSurface(src.DrawingSurface, 0, 0, _paint);
-        
-        Result.Value = workingSurface;
-    }
-
-
-    public override Node CreateCopy() => new DebugBlendModeNode();
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        _paint.Dispose();
-    }
-}

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

@@ -23,7 +23,9 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
 
     private string _lastSksl;
+    private VecI? size;
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
 
     public ModifyImageRightNode()
     {
@@ -31,8 +33,9 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         Color = CreateFuncInput(nameof(Color), "COLOR", new Half4(""));
     }
 
-    protected override void OnPaint(RenderContext renderContext, DrawingSurface targetSurface)
+    protected override void OnExecute(RenderContext renderContext)
     {
+        base.OnExecute(renderContext);
         if (OtherNode == null || OtherNode == default)
         {
             OtherNode = FindStartNode()?.Id ?? default;
@@ -47,15 +50,17 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         {
             return;
         }
-        
+
         OtherNode = startNode.Id;
 
-        if (startNode.Image.Value is not { Size: var size })
+        if (startNode.Image.Value is not { Size: var imgSize })
         {
             return;
         }
+        
+        size = imgSize;
 
-        ShaderBuilder builder = new(size);
+        ShaderBuilder builder = new(size.Value);
         FuncContext context = new(renderContext, builder);
 
         if (Coordinate.Connection != null)
@@ -102,10 +107,19 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             drawingPaint.Shader = drawingPaint.Shader.WithUpdatedUniforms(builder.Uniforms);
         }
 
-        targetSurface.Canvas.DrawRect(0, 0, size.X, size.Y, drawingPaint);
         builder.Dispose();
     }
 
+    protected override void OnPaint(RenderContext renderContext, DrawingSurface targetSurface)
+    {
+        if (size == null)
+        {
+            return;
+        }
+        
+        targetSurface.Canvas.DrawRect(0, 0, size.Value.X, size.Value.Y, drawingPaint);
+    }
+
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
         var startNode = FindStartNode();
@@ -113,7 +127,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         {
             return startNode.GetPreviewBounds(frame, elementToRenderName);
         }
-        
+
         return null;
     }
 
@@ -123,7 +137,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         if (drawingPaint != null && startNode != null && startNode.Image.Value != null)
         {
             renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
-            
+
             return true;
         }
 

+ 10 - 46
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -27,7 +27,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyList<InputProperty> InputProperties => inputs;
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
-
+    public event Action ConnectionsChanged;
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
@@ -44,9 +44,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
-
-    private Dictionary<int, Texture> _managedTextures = new();
-
+    
     public void Execute(RenderContext context)
     {
         ExecuteInternal(context);
@@ -84,43 +82,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
-    protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
-    {
-        if (_managedTextures.TryGetValue(id, out var texture))
-        {
-            if (texture.Size != size || texture.IsDisposed || texture.ColorSpace != processingCs)
-            {
-                texture.Dispose();
-                texture = new Texture(CreateImageInfo(size, processingCs));
-                _managedTextures[id] = texture;
-                return texture;
-            }
-
-            if (clear)
-            {
-                texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
-            }
-
-            return texture;
-        }
-
-        _managedTextures[id] = new Texture(CreateImageInfo(size, processingCs));
-        return _managedTextures[id];
-    }
-
-    private ImageInfo CreateImageInfo(VecI size, ColorSpace processingCs)
-    {
-        if (processingCs == null)
-        {
-            return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgbLinear())
-            {
-                GpuBacked = true
-            };
-        }
-
-        return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, processingCs) { GpuBacked = true };
-    }
-
     public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action)
     {
         var visited = new HashSet<IReadOnlyNode>();
@@ -309,6 +270,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {propName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
     }
@@ -321,6 +283,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {propName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
     }
@@ -352,6 +315,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {property.InternalPropertyName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
     }
 
@@ -384,11 +348,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 keyFrame.Dispose();
             }
         }
-
-        foreach (var texture in _managedTextures)
-        {
-            texture.Value.Dispose();
-        }
     }
 
     public void DisconnectAll()
@@ -502,6 +461,11 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         return new None();
     }
+    
+    private void InvokeConnectionsChanged()
+    {
+        ConnectionsChanged?.Invoke();
+    }
 
     private static object CloneValue(object? value, InputProperty? input)
     {

+ 23 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -1,7 +1,9 @@
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -12,6 +14,8 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
     public bool AllowHighDpiRendering { get; set; } = false;
 
+    private TextureCache textureCache = new();
+
     public RenderNode()
     {
         Painter painter = new Painter(Paint);
@@ -30,22 +34,22 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
             }
         }
     }
-    
+
     private void Paint(RenderContext context, DrawingSurface surface)
     {
         DrawingSurface target = surface;
-        bool useIntermediate = !AllowHighDpiRendering 
-                               && context.DocumentSize is { X: > 0, Y: > 0 } 
+        bool useIntermediate = !AllowHighDpiRendering
+                               && context.DocumentSize is { X: > 0, Y: > 0 }
                                && surface.DeviceClipBounds.Size != context.DocumentSize;
         if (useIntermediate)
         {
-            Texture intermediate = RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
+            Texture intermediate = textureCache.RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
         }
 
         OnPaint(context, target);
-        
-        if(useIntermediate)
+
+        if (useIntermediate)
         {
             surface.Canvas.DrawSurface(target, 0, 0);
         }
@@ -57,4 +61,17 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
     public abstract bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName);
+
+    protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
+    {
+        return textureCache.RequestTexture(id, size, processingCs, clear);
+    }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose(); 
+    }
+
+   
 }

+ 37 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs

@@ -1,29 +1,56 @@
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-public class TextureCache
+public class TextureCache : IDisposable
 {
-    private Dictionary<ChunkResolution, Texture> _cachedTextures = new();
-    
-    public Texture GetTexture(ChunkResolution resolution, VecI size)
+    private Dictionary<int, Texture> _managedTextures = new();
+
+    public Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
     {
-        if (_cachedTextures.TryGetValue(resolution, out var texture) && texture.Size == size)
+        if (_managedTextures.TryGetValue(id, out var texture))
         {
+            if (texture.Size != size || texture.IsDisposed || texture.ColorSpace != processingCs)
+            {
+                texture.Dispose();
+                texture = new Texture(CreateImageInfo(size, processingCs));
+                _managedTextures[id] = texture;
+                return texture;
+            }
+
+            if (clear)
+            {
+                texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
+            }
+
             return texture;
         }
 
-        texture = new Texture(size);
-        _cachedTextures[resolution] = texture;
-        return texture;
+        _managedTextures[id] = new Texture(CreateImageInfo(size, processingCs));
+        return _managedTextures[id];
+    }
+
+    private ImageInfo CreateImageInfo(VecI size, ColorSpace processingCs)
+    {
+        if (processingCs == null)
+        {
+            return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgbLinear())
+            {
+                GpuBacked = true
+            };
+        }
+
+        return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, processingCs) { GpuBacked = true };
     }
 
     public void Dispose()
     {
-        foreach (var texture in _cachedTextures.Values)
+        foreach (var texture in _managedTextures)
         {
-            texture.Dispose();
+            texture.Value.Dispose();
         }
     }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -114,7 +114,7 @@ internal class ActionAccumulator
                     undoBoundaryPassed || viewportRefreshRequest);
             }
 
-            previewUpdater.UpdatePreviews(undoBoundaryPassed, affectedAreas.ImagePreviewAreas.Keys, affectedAreas.MaskPreviewAreas.Keys,
+            previewUpdater.UpdatePreviews(true, affectedAreas.ImagePreviewAreas.Keys, affectedAreas.MaskPreviewAreas.Keys,
                 affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
 
             // force refresh viewports for better responsiveness

+ 9 - 0
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -150,6 +150,7 @@ internal class AffectedAreasGatherer
                 case SetActiveFrame_PassthroughAction:
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToEveryImagePreview();
+                    AddAllNodesToImagePreviews();
                     break;
                 case KeyFrameLength_ChangeInfo:
                     AddWholeCanvasToMainImage();
@@ -210,6 +211,14 @@ internal class AffectedAreasGatherer
         if (!ChangedNodes.Contains(nodeId))
             ChangedNodes.Add(nodeId);
     }
+    
+    private void AddAllNodesToImagePreviews()
+    {
+        foreach (var node in tracker.Document.NodeGraph.AllNodes)
+        {
+            AddToNodePreviews(node.Id);
+        }
+    }
 
     private void AddAllToImagePreviews(Guid memberGuid, KeyFrameTime frame, bool ignoreSelf = false)
     {

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

@@ -70,8 +70,12 @@ internal class MemberPreviewUpdater
         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.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
+        if (doc.PreviewPainter == null)
+        {
+            doc.PreviewPainter = new PreviewPainter(doc.Renderer, doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
+                doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
+        }
+
         doc.PreviewPainter.Repaint();
     }
 
@@ -102,7 +106,7 @@ internal class MemberPreviewUpdater
                     structureMemberHandler.PreviewPainter.DocumentSize = doc.SizeBindable;
                     structureMemberHandler.PreviewPainter.ProcessingColorSpace =
                         internals.Tracker.Document.ProcessingColorSpace;
-                    
+
                     structureMemberHandler.PreviewPainter.Repaint();
                 }
             }
@@ -145,7 +149,8 @@ internal class MemberPreviewUpdater
         if (internals.Tracker.Document.AnimationData.TryFindKeyFrame(cel.Id, out KeyFrame _))
         {
             KeyFrameTime frameTime = doc.AnimationHandler.ActiveFrameTime;
-            cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, doc.SizeBindable,
+            cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime,
+                doc.SizeBindable,
                 internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
             cel.PreviewPainter.Repaint();
         }
@@ -161,7 +166,8 @@ internal class MemberPreviewUpdater
             VecI documentSize = doc.SizeBindable;
 
             groupHandler.PreviewPainter =
-                new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize, processingColorSpace,
+                new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize,
+                    processingColorSpace,
                     groupHandler.Id.ToString());
             groupHandler.PreviewPainter.Repaint();
         }
@@ -222,7 +228,8 @@ internal class MemberPreviewUpdater
             {
                 if (nodeVm.ResultPainter == null)
                 {
-                    nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable, doc.AnimationHandler.ActiveFrameTime,
+                    nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable,
+                        doc.AnimationHandler.ActiveFrameTime,
                         doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
                     nodeVm.ResultPainter.Repaint();
                 }

+ 30 - 18
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -11,12 +11,14 @@ using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.Rendering;
 
-internal class SceneRenderer
+internal class SceneRenderer : IDisposable
 {
     public IReadOnlyDocument Document { get; }
     public IDocument DocumentViewModel { get; }
     public bool HighResRendering { get; set; } = true;
 
+    private Texture renderTexture;
+
     public SceneRenderer(IReadOnlyDocument trackerDocument, IDocument documentViewModel)
     {
         Document = trackerDocument;
@@ -25,7 +27,7 @@ internal class SceneRenderer
 
     public void RenderScene(DrawingSurface target, ChunkResolution resolution, string? targetOutput = null)
     {
-        if(Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
+        if (Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
         RenderOnionSkin(target, resolution, targetOutput);
         RenderGraph(target, resolution, targetOutput);
     }
@@ -33,23 +35,26 @@ internal class SceneRenderer
     private void RenderGraph(DrawingSurface target, ChunkResolution resolution, string? targetOutput)
     {
         DrawingSurface renderTarget = target;
-        Texture? texture = null;
-        
+
         if (!HighResRendering || !HighDpiRenderNodePresent(Document.NodeGraph))
         {
-            texture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
-            renderTarget = texture.DrawingSurface;
+            if (renderTexture == null || renderTexture.Size != Document.Size)
+            {
+                renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
+            }
+
+            renderTexture.DrawingSurface.Canvas.Clear();
+            renderTarget = renderTexture.DrawingSurface;
         }
 
         RenderContext context = new(renderTarget, DocumentViewModel.AnimationHandler.ActiveFrameTime,
             resolution, Document.Size, Document.ProcessingColorSpace);
         context.TargetOutput = targetOutput;
         SolveFinalNodeGraph(context.TargetOutput).Execute(context);
-        
-        if(texture != null)
+
+        if (renderTexture != null)
         {
-            target.Canvas.DrawSurface(texture.DrawingSurface, 0, 0);
-            texture.Dispose();
+            target.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
         }
     }
 
@@ -61,7 +66,7 @@ internal class SceneRenderer
         }
 
         CustomOutputNode[] outputNodes = Document.NodeGraph.AllNodes.OfType<CustomOutputNode>().ToArray();
-        
+
         foreach (CustomOutputNode outputNode in outputNodes)
         {
             if (outputNode.OutputName.Value == targetOutput)
@@ -82,10 +87,10 @@ internal class SceneRenderer
             {
                 graph.AddNode(node);
             }
-            
+
             return true;
         });
-        
+
         graph.CustomOutputNode = outputNode;
         return graph;
     }
@@ -98,9 +103,9 @@ internal class SceneRenderer
             if (n is IHighDpiRenderNode { AllowHighDpiRendering: true })
             {
                 highDpiRenderNodePresent = true;
-            } 
+            }
         });
-        
+
         return highDpiRenderNodePresent;
     }
 
@@ -116,7 +121,7 @@ internal class SceneRenderer
         double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
 
         var finalGraph = SolveFinalNodeGraph(targetOutput);
-        
+
         // Render previous frames'
         for (int i = 1; i <= animationData.OnionFrames; i++)
         {
@@ -128,7 +133,8 @@ internal class SceneRenderer
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
 
-            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace, finalOpacity);
+            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace,
+                finalOpacity);
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
         }
@@ -143,9 +149,15 @@ internal class SceneRenderer
             }
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
-            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace, finalOpacity);
+            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace,
+                finalOpacity);
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
         }
     }
+
+    public void Dispose()
+    {
+        renderTexture?.Dispose();
+    }
 }

+ 31 - 17
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -17,7 +17,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     private int frameRateBindable = 60;
     private int onionFrames = 1;
     private double onionOpacity = 50;
-    
+
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
     public IReadOnlyCollection<ICelHandler> KeyFrames => keyFrames;
@@ -25,12 +25,15 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     public IReadOnlyCollection<ICelHandler> AllCels => allCels;
 
     public event Action<int, int> ActiveFrameChanged;
-    
+
     private KeyFrameCollection keyFrames = new KeyFrameCollection();
     private List<ICelHandler> allCels = new List<ICelHandler>();
     private bool onionSkinningEnabled;
     private bool isPlayingBindable;
 
+    private int? cachedFirstFrame;
+    private int? cachedLastFrame;
+
     public int ActiveFrameBindable
     {
         get => _activeFrameBindable;
@@ -68,7 +71,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             Internals.ActionAccumulator.AddFinishedActions(new ToggleOnionSkinning_PassthroughAction(value));
         }
     }
-    
+
     public int OnionFramesBindable
     {
         get => onionFrames;
@@ -80,7 +83,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             Internals.ActionAccumulator.AddFinishedActions(new SetOnionSettings_Action(value, OnionOpacityBindable));
         }
     }
-    
+
     public double OnionOpacityBindable
     {
         get => onionOpacity;
@@ -89,10 +92,10 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             if (Document.BlockingUpdateableChangeActive)
                 return;
 
-            Internals.ActionAccumulator.AddFinishedActions(new SetOnionSettings_Action(OnionFramesBindable, value)); 
+            Internals.ActionAccumulator.AddFinishedActions(new SetOnionSettings_Action(OnionFramesBindable, value));
         }
     }
-    
+
     public bool IsPlayingBindable
     {
         get => isPlayingBindable;
@@ -105,9 +108,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         }
     }
 
-    public int FirstFrame => keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
+    public int FirstFrame => cachedFirstFrame ??= keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
 
-    public int LastFrame => keyFrames.Count > 0
+    public int LastFrame => cachedLastFrame ??= keyFrames.Count > 0
         ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable)
         : DefaultEndFrame;
 
@@ -196,19 +199,19 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         ActiveFrameChanged?.Invoke(previousFrame, newFrame);
         OnPropertyChanged(nameof(ActiveFrameBindable));
     }
-    
+
     public void SetPlayingState(bool value)
     {
         isPlayingBindable = value;
         OnPropertyChanged(nameof(IsPlayingBindable));
     }
-    
+
     public void SetOnionSkinning(bool value)
     {
         onionSkinningEnabled = value;
         OnPropertyChanged(nameof(OnionSkinningEnabledBindable));
     }
-    
+
     public void SetOnionFrames(int frames, double opacity)
     {
         onionFrames = frames;
@@ -221,9 +224,13 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     {
         if (TryFindCels(keyFrameId, out CelViewModel keyFrame))
         {
+            cachedFirstFrame = null;
+            cachedLastFrame = null;
+            
             keyFrame.SetStartFrame(newStartFrame);
             keyFrame.SetDuration(newDuration);
             keyFrames.NotifyCollectionChanged();
+
             OnPropertyChanged(nameof(FirstFrame));
             OnPropertyChanged(nameof(LastFrame));
             OnPropertyChanged(nameof(FramesCount));
@@ -261,8 +268,12 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         {
             allCels.Add(iCel);
         }
-        
+
         SortByLayers();
+        
+        cachedFirstFrame = null;
+        cachedLastFrame = null;
+        
         OnPropertyChanged(nameof(FirstFrame));
         OnPropertyChanged(nameof(LastFrame));
         OnPropertyChanged(nameof(FramesCount));
@@ -290,6 +301,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
         allCels.RemoveAll(x => x.Id == keyFrameId);
         
+        cachedFirstFrame = null;
+        cachedLastFrame = null;
+
         OnPropertyChanged(nameof(FirstFrame));
         OnPropertyChanged(nameof(LastFrame));
         OnPropertyChanged(nameof(FramesCount));
@@ -376,7 +390,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         result = default;
         return false;
     }
-    
+
     public void SortByLayers()
     {
         var allLayers = Document.StructureHelper.GetAllLayers();
@@ -384,8 +398,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         var layerKeyFrames = new List<CelGroupViewModel>();
         foreach (var layer in allLayers)
         {
-            var group = unsortedKeyFrames.FirstOrDefault(x => x is CelGroupViewModel group && group.LayerGuid == layer.Id) as CelGroupViewModel; 
-            if(group != null)
+            var group = unsortedKeyFrames.FirstOrDefault(x =>
+                x is CelGroupViewModel group && group.LayerGuid == layer.Id) as CelGroupViewModel;
+            if (group != null)
             {
                 layerKeyFrames.Insert(0, group);
             }
@@ -398,9 +413,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
                 layerKeyFrames.Add(group);
             }
         }
-        
+
         this.keyFrames = new KeyFrameCollection(layerKeyFrames);
         OnPropertyChanged(nameof(KeyFrames));
     }
-
 }

+ 44 - 11
src/PixiEditor/ViewModels/Document/CelGroupViewModel.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.ComponentModel;
 using System.Reactive.Linq;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
@@ -8,10 +9,16 @@ namespace PixiEditor.ViewModels.Document;
 
 internal class CelGroupViewModel : CelViewModel, ICelGroupHandler
 {
+    private int? cachedStartFrame;
+    private int? cachedDuration;
     public ObservableCollection<ICelHandler> Children { get; } = new ObservableCollection<ICelHandler>();
 
-    public override int StartFrameBindable => Children.Count > 0 ? Children.Min(x => x.StartFrameBindable) : 0;
-    public override int DurationBindable => Children.Count > 0 ? Children.Max(x => x.StartFrameBindable + x.DurationBindable) - StartFrameBindable : 0;
+    public override int StartFrameBindable =>
+        cachedStartFrame ??= (Children.Count > 0 ? Children.Min(x => x.StartFrameBindable) : 0);
+
+    public override int DurationBindable => cachedDuration ??= (Children.Count > 0
+        ? Children.Max(x => x.StartFrameBindable + x.DurationBindable) - StartFrameBindable
+        : 0);
 
     public string LayerName => Document.StructureHelper.Find(LayerGuid).NodeNameBindable;
 
@@ -37,16 +44,17 @@ internal class CelGroupViewModel : CelViewModel, ICelGroupHandler
     {
         foreach (var child in Children)
         {
-            if(child is CelViewModel keyFrame)
+            if (child is CelViewModel keyFrame)
             {
                 keyFrame.SetVisibility(isVisible);
             }
         }
-        
+
         base.SetVisibility(isVisible);
     }
 
-    public CelGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
+    public CelGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc,
+        DocumentInternalParts internalParts)
         : base(startFrame, duration, layerGuid, id, doc, internalParts)
     {
         Children.CollectionChanged += ChildrenOnCollectionChanged;
@@ -61,19 +69,44 @@ internal class CelGroupViewModel : CelViewModel, ICelGroupHandler
 
     private void ChildrenOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {
-        OnPropertyChanged(nameof(StartFrameBindable));
-        OnPropertyChanged(nameof(DurationBindable));
-        
+        cachedStartFrame = null;
+        cachedDuration = null;
+
         if (e.Action == NotifyCollectionChangedAction.Add)
         {
             foreach (var item in e.NewItems)
             {
-                if (item is CelViewModel keyFrame)
+                if (item is CelViewModel cel)
+                {
+                    cel.IsCollapsed = IsCollapsed;
+                    cel.SetVisibility(IsVisible);
+                    cel.PropertyChanged += CelOnPropertyChanged;
+                }
+            }
+        }
+        else if (e.Action == NotifyCollectionChangedAction.Remove)
+        {
+            foreach (var item in e.OldItems)
+            {
+                if (item is CelViewModel cel)
                 {
-                    keyFrame.IsCollapsed = IsCollapsed;
-                    keyFrame.SetVisibility(IsVisible);
+                    cel.PropertyChanged -= CelOnPropertyChanged;
                 }
             }
         }
+
+        OnPropertyChanged(nameof(StartFrameBindable));
+        OnPropertyChanged(nameof(DurationBindable));
+    }
+    
+    private void CelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName is nameof(ICelHandler.StartFrameBindable) or nameof(ICelHandler.DurationBindable))
+        {
+            cachedStartFrame = null;
+            cachedDuration = null;
+            OnPropertyChanged(nameof(StartFrameBindable));
+            OnPropertyChanged(nameof(DurationBindable));
+        }
     }
 }

+ 1 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -1001,5 +1001,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         Internals.Tracker.Dispose();
         Internals.Tracker.Document.Dispose();
+        SceneRenderer.Dispose();
     }
 }

+ 0 - 7
src/PixiEditor/ViewModels/Document/Nodes/DebugBlendModeNodeViewModel.cs

@@ -1,7 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using PixiEditor.ViewModels.Nodes;
-
-namespace PixiEditor.ViewModels.Document.Nodes;
-
-[NodeViewModel("Debug Blend Mode", "", null)]
-internal class DebugBlendModeNodeViewModel : NodeViewModel<DebugBlendModeNode>;