Browse Source

Merge branch 'master' into crash-report-api

CPK 7 months ago
parent
commit
0f3a7bf905
27 changed files with 452 additions and 295 deletions
  1. 1 1
      src/Directory.Build.props
  2. 1 1
      src/Drawie
  3. 1 1
      src/PixiDocks
  4. 51 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  5. 48 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  6. 23 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  7. 50 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  8. 0 57
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  9. 13 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  10. 10 46
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  11. 23 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  12. 37 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs
  13. 2 1
      src/PixiEditor.UI.Common/Controls/ComboBox.axaml
  14. 0 31
      src/PixiEditor.sln
  15. 2 2
      src/PixiEditor/Helpers/VersionHelpers.cs
  16. 1 1
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  17. 9 0
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  18. 67 24
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  19. 30 18
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  20. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  21. 31 17
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  22. 44 11
      src/PixiEditor/ViewModels/Document/CelGroupViewModel.cs
  23. 1 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  24. 0 7
      src/PixiEditor/ViewModels/Document/Nodes/DebugBlendModeNodeViewModel.cs
  25. 3 3
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  26. 1 1
      tests/Directory.Build.props
  27. 1 1
      windows-x64-release-dev.yml

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.2.2</AvaloniaVersion>
+		    <AvaloniaVersion>11.2.3</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 63c826db08b9ea57cf6ae29718d94fd77951c73a
+Subproject commit 94655abbd38090a174750b61a51a2e74c056bfe6

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 9190dfc8d59f5eaae40a80e34e75a1d5667dec83
+Subproject commit 0d356fcd1f07aa2b1c274284a3f81d302c593db6

+ 51 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -11,8 +11,10 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncInputProperty
 {
     private T? constantNonOverrideValue;
-    
-    internal FuncInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, null)
+    private int lastConstantHashCode;
+
+    internal FuncInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node,
+        internalName, displayName, null)
     {
         constantNonOverrideValue = defaultValue;
         NonOverridenValue = _ => constantNonOverrideValue;
@@ -28,7 +30,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
                 shaderExpressionVariable.SetConstantValue(toReturn, ConversionTable.Convert);
                 return (T)(object)shaderExpressionVariable;
             }
-            
+
             return (T)toReturn;
         };
         return func;
@@ -40,12 +42,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         {
             Type targetType = typeof(T);
             bool isShaderExpression = false;
-            if(typeof(T).IsAssignableTo(typeof(ShaderExpressionVariable)))
+            if (typeof(T).IsAssignableTo(typeof(ShaderExpressionVariable)))
             {
                 targetType = targetType.BaseType.GenericTypeArguments[0];
                 isShaderExpression = true;
             }
-            
+
             var sourceObj = delegateToCast.DynamicInvoke(f);
             ConversionTable.TryConvert(sourceObj, targetType, out var result);
             if (isShaderExpression)
@@ -67,12 +69,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
                 return (T)toReturn;
             }
-            
-            return result == null ? default : (T)result; 
+
+            return result == null ? default : (T)result;
         };
         return func;
     }
-    
+
     private Expression Adjust(Expression expression, object toReturn, out bool adjustNestedVariables)
     {
         adjustNestedVariables = false;
@@ -89,7 +91,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
         return expression;
     }
-    
+
     private void AdjustNested(IMultiValueVariable toReturn, Expression expression)
     {
         if (toReturn is not ShaderExpressionVariable shaderExpressionVariable)
@@ -133,8 +135,8 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
             shaderExpressionVariable.SetConstantValue(value, ConversionTable.Convert);
             return;
         }
-        
-        if(ConversionTable.TryConvert(value, typeof(T), out var result))
+
+        if (ConversionTable.TryConvert(value, typeof(T), out var result))
         {
             constantNonOverrideValue = (T)result;
             return;
@@ -142,4 +144,42 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
         constantNonOverrideValue = default;
     }
+
+    internal override bool CacheChanged
+    {
+        get
+        {
+            if (constantNonOverrideValue == null)
+            {
+                return base.CacheChanged;
+            }
+
+            if (Connection == null && lastConnectionHash != -1)
+            {
+                return true;
+            }
+
+            if (Connection != null && lastConnectionHash != Connection.GetHashCode())
+            {
+                lastConnectionHash = Connection.GetHashCode();
+                return true;
+            }
+
+            if (constantNonOverrideValue is ShaderExpressionVariable expressionVariable)
+            {
+                return expressionVariable.ConstantValueString.GetHashCode() != lastConstantHashCode;
+            }
+
+            return base.CacheChanged;
+        }
+    }
+
+    internal override void UpdateCache()
+    {
+        base.UpdateCache();
+        if (constantNonOverrideValue is ShaderExpressionVariable expressionVariable)
+        {
+            lastConstantHashCode = expressionVariable.ConstantValueString.GetHashCode();
+        }
+    }
 }

+ 48 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -11,8 +11,12 @@ public class InputProperty : IInputProperty
 {
     private object _internalValue;
     private int _lastExecuteHash = -1;
+    protected int lastConnectionHash = -1;
     private PropertyValidator? validator;
-    
+    private IOutputProperty? connection;
+
+    public event Action ConnectionChanged;
+
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
 
@@ -26,7 +30,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 +44,7 @@ public class InputProperty : IInputProperty
             return connectionValue;
         }
     }
-    
+
     public object NonOverridenValue
     {
         get => _internalValue;
@@ -49,7 +53,7 @@ public class InputProperty : IInputProperty
             _internalValue = value;
         }
     }
-    
+
     public PropertyValidator Validator
     {
         get
@@ -73,28 +77,42 @@ 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; } 
-    internal bool CacheChanged
+    public Type ValueType { get; }
+
+    internal virtual bool CacheChanged
     {
         get
         {
+            if(Connection == null && lastConnectionHash != -1)
+            {
+                return true;
+            }
+            
+            if(Connection != null && lastConnectionHash != Connection.GetHashCode())
+            {
+                lastConnectionHash = Connection.GetHashCode();
+                return true;
+            }
+            
             if (Value is ICacheable cacheable)
             {
                 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;
             }
@@ -103,7 +121,7 @@ public class InputProperty : IInputProperty
         }
     }
 
-    internal void UpdateCache()
+    internal virtual void UpdateCache()
     {
         if (Value is null)
         {
@@ -117,12 +135,25 @@ public class InputProperty : IInputProperty
         {
             _lastExecuteHash = Value.GetHashCode();
         }
+        
+        lastConnectionHash = Connection?.GetHashCode() ?? -1;
     }
-    
+
     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 +164,6 @@ public class InputProperty : IInputProperty
     }
 }
 
-
 public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
     public new T Value
@@ -150,9 +180,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;
+    }
 }

+ 50 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -5,11 +5,12 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("CreateImage")]
-public class CreateImageNode : Node
+public class CreateImageNode : Node, IPreviewRenderable
 {
     public OutputProperty<Texture> Output { get; }
 
@@ -21,6 +22,8 @@ public class CreateImageNode : Node
 
     public RenderOutputProperty RenderOutput { get; }
 
+    private TextureCache textureCache = new();
+
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
@@ -37,7 +40,16 @@ public class CreateImageNode : Node
             return;
         }
 
-        var surface = RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
+        var surface = Render(context);
+
+        Output.Value = surface;
+
+        RenderOutput.ChainToPainterValue();
+    }
+
+    private Texture Render(RenderContext context)
+    {
+        var surface = textureCache.RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
 
         surface.DrawingSurface.Canvas.Clear(Fill.Value);
 
@@ -49,10 +61,7 @@ public class CreateImageNode : Node
         Content.Value?.Paint(ctx, surface.DrawingSurface);
 
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
-        
-        Output.Value = surface;
-
-        RenderOutput.ChainToPainterValue();
+        return surface;
     }
 
     private void OnPaint(RenderContext context, DrawingSurface surface)
@@ -61,4 +70,39 @@ public class CreateImageNode : Node
     }
 
     public override Node CreateCopy() => new CreateImageNode();
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose();
+    }
+
+    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        {
+            return null;
+        }
+
+        return new RectD(0, 0, Size.Value.X, Size.Value.Y);
+    }
+
+    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);
+        
+        renderOn.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+        
+        return true;
+    }
 }

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

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

@@ -23,7 +23,11 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
 
     private string _lastSksl;
+    private VecI? size;
 
+    // TODO: Add caching
+    // Caching requires a way to check if any connected node changed, checking inputs for this node works
+    // Also gather uniforms without doing full string builder generation of the shader
 
     public ModifyImageRightNode()
     {
@@ -47,15 +51,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;
         }
 
-        ShaderBuilder builder = new(size);
+        size = imgSize;
+
+        ShaderBuilder builder = new(size.Value);
         FuncContext context = new(renderContext, builder);
 
         if (Coordinate.Connection != null)
@@ -102,7 +108,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             drawingPaint.Shader = drawingPaint.Shader.WithUpdatedUniforms(builder.Uniforms);
         }
 
-        targetSurface.Canvas.DrawRect(0, 0, size.X, size.Y, drawingPaint);
+        targetSurface.Canvas.DrawRect(0, 0, size.Value.X, size.Value.Y, drawingPaint);
         builder.Dispose();
     }
 
@@ -113,17 +119,17 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         {
             return startNode.GetPreviewBounds(frame, elementToRenderName);
         }
-        
+
         return null;
     }
 
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var startNode = FindStartNode();
-        if (drawingPaint != null && startNode != null && startNode.Image.Value != null)
+        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);
-            
+
             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();
         }
     }
 }

+ 2 - 1
src/PixiEditor.UI.Common/Controls/ComboBox.axaml

@@ -33,7 +33,8 @@
             <ContentControl Margin="{TemplateBinding Padding}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
-                            Content="{TemplateBinding SelectionBoxItem}"
+                            Content="{Binding SelectionBoxItem,
+                                              RelativeSource={RelativeSource TemplatedParent}}"
                             ContentTemplate="{TemplateBinding ItemTemplate}" />
             <ToggleButton Name="toggle"
                           Grid.Column="1"

+ 0 - 31
src/PixiEditor.sln

@@ -124,8 +124,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi", "Drawie\
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi.Vulkan", "Drawie\src\Drawie.RenderApi.Vulkan\Drawie.RenderApi.Vulkan.csproj", "{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpirvCompiler", "Drawie\src\SpirvCompiler\SpirvCompiler.csproj", "{475C7BBF-B10B-456A-A095-4395E49CF4B2}"
-EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Interop.Avalonia", "Drawie\src\Drawie.Interop.Avalonia\Drawie.Interop.Avalonia.csproj", "{6D79C3E3-E31F-43B4-B173-3E6959230923}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Interop.Avalonia.OpenGl", "Drawie\src\Drawie.Interop.Avalonia.OpenGl\Drawie.Interop.Avalonia.OpenGl.csproj", "{843F55B4-987B-45A9-BDBD-1A0A86CB883E}"
@@ -1075,34 +1073,6 @@ Global
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}.Steam|x64.Build.0 = Debug|Any CPU
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}.Steam|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|x64.ActiveCfg = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|x64.Build.0 = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|ARM64.ActiveCfg = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|ARM64.Build.0 = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|ARM64.Build.0 = Debug|Any CPU
 		{6D79C3E3-E31F-43B4-B173-3E6959230923}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{6D79C3E3-E31F-43B4-B173-3E6959230923}.Debug|x64.Build.0 = Debug|Any CPU
 		{6D79C3E3-E31F-43B4-B173-3E6959230923}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -1327,7 +1297,6 @@ Global
 		{467EDEB3-6004-46B8-8448-2F5C4F131D75} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{21152CEE-F9D7-452F-9FF5-B15FF80F2CED} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{6D79C3E3-E31F-43B4-B173-3E6959230923} = {E8A74431-F76F-43B1-BC66-CA05E249E6F4}
 		{843F55B4-987B-45A9-BDBD-1A0A86CB883E} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{7BD495CA-2EB5-4ABC-BDDB-0E1765C40C19} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}

+ 2 - 2
src/PixiEditor/Helpers/VersionHelpers.cs

@@ -42,10 +42,10 @@ internal static class VersionHelpers
         return "BetaDebug";
 #elif DEVRELEASE
         return "BetaDevRelease";
-#elif RELEASE
-        return "BetaRelease";
 #elif STEAM
         return "BetaSteam";
+#elif RELEASE
+        return "BetaRelease";
 #elif MSIX
         return "BetaMSIX";
 #else

+ 1 - 1
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -194,7 +194,7 @@ internal static class ClipboardController
 
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
 
-        if (layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
+        if (pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
         {
             foreach (var layerId in layerIds)
             {

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

+ 67 - 24
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -33,13 +33,10 @@ internal class MemberPreviewUpdater
         AnimationKeyFramePreviewRenderer = new AnimationKeyFramePreviewRenderer(internals);
     }
 
-    public void UpdatePreviews(bool rerenderPreviews, IEnumerable<Guid> membersToUpdate,
+    public void UpdatePreviews(bool undoBoundaryPassed, IEnumerable<Guid> membersToUpdate,
         IEnumerable<Guid> masksToUpdate, IEnumerable<Guid> nodesToUpdate, IEnumerable<Guid> keyFramesToUpdate)
     {
-        if (!rerenderPreviews)
-            return;
-
-        UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate);
+        UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate, undoBoundaryPassed);
     }
 
     /// <summary>
@@ -48,17 +45,22 @@ internal class MemberPreviewUpdater
     /// <param name="members">Members that should be rendered</param>
     /// <param name="masksToUpdate">Masks that should be rendered</param>
     private void UpdatePreviewPainters(IEnumerable<Guid> members, IEnumerable<Guid> masksToUpdate,
-        IEnumerable<Guid> nodesToUpdate, IEnumerable<Guid> keyFramesToUpdate)
+        IEnumerable<Guid> nodesToUpdate, IEnumerable<Guid> keyFramesToUpdate, bool undoBoundaryPassed)
     {
         Guid[] memberGuids = members as Guid[] ?? members.ToArray();
         Guid[] maskGuids = masksToUpdate as Guid[] ?? masksToUpdate.ToArray();
         Guid[] nodesGuids = nodesToUpdate as Guid[] ?? nodesToUpdate.ToArray();
         Guid[] keyFramesGuids = keyFramesToUpdate as Guid[] ?? keyFramesToUpdate.ToArray();
 
-        RenderWholeCanvasPreview();
+        if (undoBoundaryPassed)
+        {
+            RenderWholeCanvasPreview();
+        }
+
         RenderLayersPreview(memberGuids);
         RenderMaskPreviews(maskGuids);
         RenderAnimationPreviews(memberGuids, keyFramesGuids);
+
         RenderNodePreviews(nodesGuids);
     }
 
@@ -70,8 +72,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 +108,7 @@ internal class MemberPreviewUpdater
                     structureMemberHandler.PreviewPainter.DocumentSize = doc.SizeBindable;
                     structureMemberHandler.PreviewPainter.ProcessingColorSpace =
                         internals.Tracker.Document.ProcessingColorSpace;
-                    
+
                     structureMemberHandler.PreviewPainter.Repaint();
                 }
             }
@@ -145,8 +151,19 @@ 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,
-                internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
+            if (cel.PreviewPainter == null)
+            {
+                cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime,
+                    doc.SizeBindable,
+                    internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
+            }
+            else
+            {
+                cel.PreviewPainter.FrameTime = frameTime;
+                cel.PreviewPainter.DocumentSize = doc.SizeBindable;
+                cel.PreviewPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
+            }
+
             cel.PreviewPainter.Repaint();
         }
     }
@@ -160,9 +177,20 @@ internal class MemberPreviewUpdater
             ColorSpace processingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
             VecI documentSize = doc.SizeBindable;
 
-            groupHandler.PreviewPainter =
-                new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize, processingColorSpace,
-                    groupHandler.Id.ToString());
+            if (groupHandler.PreviewPainter == null)
+            {
+                groupHandler.PreviewPainter =
+                    new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize,
+                        processingColorSpace,
+                        groupHandler.Id.ToString());
+            }
+            else
+            {
+                groupHandler.PreviewPainter.FrameTime = frameTime;
+                groupHandler.PreviewPainter.DocumentSize = documentSize;
+                groupHandler.PreviewPainter.ProcessingColorSpace = processingColorSpace;
+            }
+
             groupHandler.PreviewPainter.Repaint();
         }
     }
@@ -180,13 +208,24 @@ internal class MemberPreviewUpdater
                 if (member is not IPreviewRenderable previewRenderable)
                     continue;
 
-                structureMemberHandler.MaskPreviewPainter = new PreviewPainter(
-                    doc.Renderer,
-                    previewRenderable,
-                    doc.AnimationHandler.ActiveFrameTime,
-                    doc.SizeBindable,
-                    internals.Tracker.Document.ProcessingColorSpace,
-                    nameof(StructureNode.EmbeddedMask));
+                if (structureMemberHandler.MaskPreviewPainter == null)
+                {
+                    structureMemberHandler.MaskPreviewPainter = new PreviewPainter(
+                        doc.Renderer,
+                        previewRenderable,
+                        doc.AnimationHandler.ActiveFrameTime,
+                        doc.SizeBindable,
+                        internals.Tracker.Document.ProcessingColorSpace,
+                        nameof(StructureNode.EmbeddedMask));
+                }
+                else
+                {
+                    structureMemberHandler.MaskPreviewPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
+                    structureMemberHandler.MaskPreviewPainter.DocumentSize = doc.SizeBindable;
+                    structureMemberHandler.MaskPreviewPainter.ProcessingColorSpace =
+                        internals.Tracker.Document.ProcessingColorSpace;
+                }
+
                 structureMemberHandler.MaskPreviewPainter.Repaint();
             }
         }
@@ -202,7 +241,10 @@ internal class MemberPreviewUpdater
         var executionQueue =
             internals.Tracker.Document.NodeGraph
                 .AllNodes; //internals.Tracker.Document.NodeGraph.CalculateExecutionQueue(outputNode);
-
+        
+        if(nodesGuids.Length == 0)
+            return;
+        
         foreach (var node in executionQueue)
         {
             if (node is null)
@@ -222,7 +264,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();
+    }
 }

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

@@ -41,5 +41,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.0.42")]
-[assembly: AssemblyFileVersion("2.0.0.42")]
+[assembly: AssemblyVersion("2.0.0.43")]
+[assembly: AssemblyFileVersion("2.0.0.43")]

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

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

@@ -46,12 +46,12 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         doc.Operations.DeleteSelectedPixels(doc.AnimationDataViewModel.ActiveFrameBindable, true);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE",
-        CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
-        MenuItemPath = "EDIT/PASTE", MenuItemOrder = 4, Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Clipboard.PasteAsNewLayer", true, "PASTE_AS_NEW_LAYER", "PASTE_AS_NEW_LAYER_DESCRIPTIVE",
         CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Control,
         Icon = PixiPerfectIcons.PasteAsNewLayer, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
+        MenuItemPath = "EDIT/PASTE", MenuItemOrder = 4, Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
     public void Paste(bool pasteAsNewLayer)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)

+ 1 - 1
tests/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		<AvaloniaVersion>11.2.2</AvaloniaVersion>
+		<AvaloniaVersion>11.2.3</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 1 - 1
windows-x64-release-dev.yml

@@ -111,7 +111,7 @@ steps:
   displayName: Publish PixiEditor
   inputs:
     filePath: 'src/PixiEditor.Builder/build.ps1'
-    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor.Desktop" --build-configuration "$(buildConfiguration)" --runtime "$(buildPlatform)" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-$(architecture)-light\PixiEditor" --analytics-url "https://api.pixieditor.net/analytics/" --project-path C:\Git\PixiEditor\src\PixiEditor.Desktop --extension-projects "$(System.DefaultWorkingDirectory)\src\PixiEditor.Beta" --crash-report-webhook-url "$(crash-webhook-url)"'
+    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor.Desktop" --build-configuration "$(buildConfiguration)" --runtime "$(buildPlatform)" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-$(architecture)-light\PixiEditor" --analytics-url "https://api.pixieditor.net/analytics/" --extension-projects "$(System.DefaultWorkingDirectory)\src\PixiEditor.Beta" --crash-report-webhook-url "$(crash-webhook-url)"'
     workingDirectory: 'src/PixiEditor.Builder'
 
 - task: ArchiveFiles@2