Browse Source

Some major fixes but it's not working for all cases yet

Krzysztof Krysiński 1 month ago
parent
commit
6007707a21

+ 3 - 0
src/PixiEditor.ChangeableDocument/AssemblyInfo.cs

@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("PixiEditor.Backend.Tests")]

+ 8 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/GraphUtils.cs

@@ -74,7 +74,10 @@ public static class GraphUtils
             }
         }
 
-        finalQueue = new HashSet<IReadOnlyNode>(finalQueue.Except(nodesToExclude));
+        if (nodesToExclude.Count > 0)
+        {
+            finalQueue = new HashSet<IReadOnlyNode>(finalQueue.Except(nodesToExclude));
+        }
 
         return new Queue<IReadOnlyNode>(finalQueue);
     }
@@ -153,7 +156,10 @@ public static class GraphUtils
         if(ignoreOutputFlowNode && outputNode is IExecutionFlowNode)
             nodesToExclude.Add(outputNode);
 
-        finalQueue = new HashSet<IReadOnlyNode>(finalQueue.Except(nodesToExclude));
+        if (nodesToExclude.Count > 0)
+        {
+            finalQueue = new HashSet<IReadOnlyNode>(finalQueue.Except(nodesToExclude));
+        }
 
         return new Queue<IReadOnlyNode>(finalQueue);
     }

+ 58 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperties.cs

@@ -0,0 +1,58 @@
+using System.Collections;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public class InputProperties : IReadOnlyInputProperties
+{
+    public Dictionary<string, InputProperty> Properties { get; } = new();
+
+    public void Add(InputProperty property)
+    {
+        if (!Properties.TryAdd(property.InternalPropertyName, property))
+            throw new ArgumentException($"Property with name {property.InternalPropertyName} already exists.");
+    }
+
+    public void Add<T>(InputProperty<T> property)
+    {
+        if (!Properties.TryAdd(property.InternalPropertyName, property))
+            throw new ArgumentException($"Property with name {property.InternalPropertyName} already exists.");
+    }
+
+    public bool Remove(InputProperty property)
+    {
+        return Properties.Remove(property.InternalPropertyName);
+    }
+
+    public IEnumerator<IInputProperty> GetEnumerator()
+    {
+        return Properties.Values.GetEnumerator();
+    }
+
+    public int Count => Properties.Count;
+
+    public InputProperty? TryGetProperty(string internalName)
+    {
+        Properties.TryGetValue(internalName, out var prop);
+        return prop;
+    }
+
+    IInputProperty IReadOnlyInputProperties.TryGetProperty(string internalName)
+    {
+        return TryGetProperty(internalName);
+    }
+
+    IEnumerator IEnumerable.GetEnumerator()
+    {
+        return GetEnumerator();
+    }
+
+    public IInputProperty TryGetPropertyc { get; set; }
+}
+
+public interface IReadOnlyInputProperties : IEnumerable<IInputProperty>
+{
+    int Count { get; }
+    public IInputProperty TryGetProperty(string internalName);
+}
+

+ 28 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
@@ -16,6 +17,7 @@ public class InputProperty : IInputProperty
     private PropertyValidator? validator;
     private IOutputProperty? connection;
     private Dictionary<Guid, IOutputProperty> virtualConnections = new();
+    private Dictionary<Guid, RenderContext> virtualConnectionContexts = new();
     private Dictionary<Guid, object> virtualNonOverridenValues = new();
 
     public event Action ConnectionChanged;
@@ -239,6 +241,11 @@ public class InputProperty : IInputProperty
     public void SetVirtualConnection(IOutputProperty outputProperty, Guid virtualConnectionId, RenderContext context)
     {
         virtualConnections[virtualConnectionId] = outputProperty;
+        if(virtualConnectionContexts.TryGetValue(virtualConnectionId, out var existingContext) && existingContext != context)
+        {
+            throw new InvalidOperationException("A virtual connection can only be associated with one RenderContext.");
+        }
+        virtualConnectionContexts[virtualConnectionId] = context;
         context.RecordVirtualConnection(this, virtualConnectionId);
     }
 
@@ -256,9 +263,16 @@ public class InputProperty : IInputProperty
         HashCode hash = new();
         hash.Add(InternalPropertyName);
         hash.Add(ValueType);
+        Stopwatch sw = Stopwatch.StartNew();
+        sw.Start();
         if (Value is ICacheable cacheable)
         {
             hash.Add(cacheable.GetCacheHash());
+            if(sw.ElapsedMilliseconds > 50)
+            {
+                Debug.WriteLine($"Long cache hash calculation in InputProperty {Node.GetType().Name}.{InternalPropertyName}");
+            }
+            sw.Stop();
         }
         else if (Value is Delegate func && Connection == null)
         {
@@ -274,16 +288,22 @@ public class InputProperty : IInputProperty
         }
         else
         {
-            hash.Add(Value?.GetHashCode() ?? 0);
+            hash.Add(NonOverridenValue?.GetHashCode() ?? 0);
         }
 
         hash.Add(Connection?.GetCacheHash() ?? 0);
+
         return hash.ToHashCode();
     }
 
     public void SetVirtualNonOverridenValue<T>(T value, Guid virtualConnectionId, RenderContext context)
     {
-        virtualNonOverridenValues[virtualConnectionId] = CastValue(value);
+        virtualNonOverridenValues[virtualConnectionId] = value == null ? null : CastValue(value);
+        if(virtualConnectionContexts.TryGetValue(virtualConnectionId, out var existingContext) && existingContext != context)
+        {
+            throw new InvalidOperationException("A virtual connection can only be associated with one RenderContext.");
+        }
+        virtualConnectionContexts[virtualConnectionId] = context;
         context.RecordVirtualNonOverridenValue(this, virtualConnectionId);
     }
 
@@ -291,6 +311,11 @@ public class InputProperty : IInputProperty
     {
         virtualNonOverridenValues.Remove(virtualSessionId);
     }
+
+    public RenderContext? GetVirtualContext(Guid virtualSession)
+    {
+        return virtualConnectionContexts.GetValueOrDefault(virtualSession);
+    }
 }
 
 public class InputProperty<T> : InputProperty, IInputProperty<T>

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

@@ -26,6 +26,7 @@ public interface IInputProperty : INodeProperty
     public IOutputProperty? Connection { get; set; }
     public object NonOverridenValue { get; set;  }
     public void RemoveVirtualConnection(Guid virtualConnectionId);
+    public void SetVirtualNonOverridenValue<T>(T value, Guid virtualSession, RenderContext context);
 }
 
 public interface IOutputProperty : INodeProperty

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

@@ -10,8 +10,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 public interface IReadOnlyNode : ICacheable
 {
     public Guid Id { get; }
-    public IReadOnlyList<IInputProperty> InputProperties { get; }
-    public IReadOnlyList<IOutputProperty> OutputProperties { get; }
+    public IReadOnlyInputProperties InputProperties { get; }
+    public IReadOnlyOutputProperties OutputProperties { get; }
     public IReadOnlyList<IReadOnlyKeyFrameData> KeyFrames { get; }
     public VecD Position { get; }
     string DisplayName { get; }

+ 38 - 37
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -19,18 +19,18 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 public abstract class Node : IReadOnlyNode, IDisposable
 {
     private string displayName;
-    private List<InputProperty> inputs = new();
-    private List<OutputProperty> outputs = new();
+    private InputProperties inputs = new();
+    private OutputProperties outputs = new();
     protected List<KeyFrameData> keyFrames = new();
     public Guid Id { get; internal set; } = Guid.NewGuid();
 
-    public IReadOnlyList<InputProperty> InputProperties => inputs;
-    public IReadOnlyList<OutputProperty> OutputProperties => outputs;
+    public InputProperties InputProperties => inputs;
+    public OutputProperties OutputProperties => outputs;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
     public event Action ConnectionsChanged;
 
-    IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
-    IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
+    IReadOnlyInputProperties IReadOnlyNode.InputProperties => inputs;
+    IReadOnlyOutputProperties IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyList<IReadOnlyKeyFrameData> IReadOnlyNode.KeyFrames => keyFrames;
     public VecD Position { get; set; }
 
@@ -87,7 +87,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Inputs))
         {
-            changed |= inputs.Any(x => x.CacheChanged);
+            changed |= inputs.Properties.Any(x => x.Value.CacheChanged);
         }
 
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Timeline))
@@ -105,7 +105,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected virtual void UpdateCache(RenderContext context)
     {
-        foreach (var input in inputs)
+        foreach (var input in inputs.Properties.Values)
         {
             input.UpdateCache();
         }
@@ -117,7 +117,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
         lastContentCacheHash = GetContentCacheHash();
     }
 
-    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action, Func<IInputProperty, bool>? branchCondition = null)
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action,
+        Func<IInputProperty, bool>? branchCondition = null)
     {
         var visited = new HashSet<IReadOnlyNode>();
         var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
@@ -143,6 +144,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 {
                     continue;
                 }
+
                 if (inputProperty.Connection != null)
                 {
                     queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
@@ -364,14 +366,10 @@ public abstract class Node : IReadOnlyNode, IDisposable
     }
 
 
-    protected FuncInputProperty<T, TContext> CreateFuncInput<T, TContext>(string propName, string displayName, T defaultValue) where TContext : FuncContext
+    protected FuncInputProperty<T, TContext> CreateFuncInput<T, TContext>(string propName, string displayName,
+        T defaultValue) where TContext : FuncContext
     {
         var property = new FuncInputProperty<T, TContext>(this, propName, displayName, defaultValue);
-        if (InputProperties.Any(x => x.InternalPropertyName == propName))
-        {
-            throw new InvalidOperationException($"Input with name {propName} already exists.");
-        }
-
         property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
@@ -380,10 +378,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
     protected InputProperty<T> CreateInput<T>(string propName, string displayName, T defaultValue)
     {
         var property = new InputProperty<T>(this, propName, displayName, defaultValue);
-        if (InputProperties.Any(x => x.InternalPropertyName == propName))
-        {
-            throw new InvalidOperationException($"Input with name {propName} already exists.");
-        }
 
         property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
@@ -420,11 +414,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected void AddInputProperty(InputProperty property)
     {
-        if (InputProperties.Any(x => x.InternalPropertyName == property.InternalPropertyName))
-        {
-            throw new InvalidOperationException($"Input with name {property.InternalPropertyName} already exists.");
-        }
-
         property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
     }
@@ -433,7 +422,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         _isDisposed = true;
         DisconnectAll();
-        foreach (var input in inputs)
+        foreach (var input in inputs.Properties.Values)
         {
             if (input is { Connection: null, NonOverridenValue: IDisposable disposable })
             {
@@ -442,7 +431,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             }
         }
 
-        foreach (var output in outputs)
+        foreach (var output in outputs.Properties.Values)
         {
             if (output.Connections.Count == 0 && output.Value is IDisposable disposable)
             {
@@ -462,12 +451,12 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     public void DisconnectAll()
     {
-        foreach (var input in inputs)
+        foreach (var input in inputs.Properties.Values)
         {
             input.Connection?.DisconnectFrom(input);
         }
 
-        foreach (var output in outputs)
+        foreach (var output in outputs.Properties.Values)
         {
             var connections = output.Connections.ToArray();
             for (var i = 0; i < connections.Length; i++)
@@ -499,11 +488,24 @@ public abstract class Node : IReadOnlyNode, IDisposable
         clone.Id = preserveGuids ? Id : Guid.NewGuid();
         clone.Position = Position;
 
-        for (var i = 0; i < clone.inputs.Count; i++)
+        foreach (var prop in inputs.Properties)
         {
-            var toClone = inputs[i];
-            object value = CloneValue(toClone.NonOverridenValue, clone.inputs[i]);
-            clone.inputs[i].NonOverridenValue = value;
+            var inputs = clone.inputs.Properties;
+            var virtualSession = prop.Value.ActiveVirtualSession;
+            if (prop.Value.ActiveVirtualSession != null)
+            {
+                prop.Value.ActiveVirtualSession = null;
+            }
+
+            var toClone = prop.Value;
+            object value = CloneValue(toClone.NonOverridenValue, clone.inputs.Properties[prop.Key]);
+            clone.inputs.Properties[prop.Key].NonOverridenValue = value;
+
+            if (virtualSession != null)
+            {
+                prop.Value.ActiveVirtualSession = virtualSession;
+                clone.inputs.Properties[prop.Key].SetVirtualNonOverridenValue(CloneValue(toClone.NonOverridenValue, clone.inputs.Properties[prop.Key]), virtualSession.Value, prop.Value.GetVirtualContext(virtualSession.Value));
+            }
         }
 
         // This makes shader outputs copy old delegate, also I don't think it's required because output is calculated based on inputs,
@@ -534,23 +536,23 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     public InputProperty? GetInputProperty(string inputProperty)
     {
-        return inputs.FirstOrDefault(x => x.InternalPropertyName == inputProperty);
+        return inputs.Properties.GetValueOrDefault(inputProperty);
     }
 
     public OutputProperty? GetOutputProperty(string outputProperty)
     {
-        return outputs.FirstOrDefault(x => x.InternalPropertyName == outputProperty);
+        return outputs.Properties.GetValueOrDefault(outputProperty);
     }
 
 
     public bool HasInputProperty(string propertyName)
     {
-        return inputs.Any(x => x.InternalPropertyName == propertyName);
+        return inputs.Properties.ContainsKey(propertyName);
     }
 
     public bool HasOutputProperty(string propertyName)
     {
-        return outputs.Any(x => x.InternalPropertyName == propertyName);
+        return outputs.Properties.ContainsKey(propertyName);
     }
 
     IInputProperty? IReadOnlyNode.GetInputProperty(string inputProperty)
@@ -629,7 +631,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
 
         hash.Add(GetContentCacheHash());
-
         return hash.ToHashCode();
     }
 }

+ 46 - 26
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/RepeatNodeEnd.cs

@@ -17,16 +17,16 @@ public class RepeatNodeEnd : Node, IPairNode, IExecutionFlowNode
     {
         get
         {
-            startNode = FindStartNode();
+            startNode = FindStartNode(out _);
             return startNode?.Id ?? Guid.Empty;
         }
         set
         {
-           // no op, the start node is found dynamically
+            // no op, the start node is found dynamically
         }
     }
 
-    private RepeatNodeStart startNode;
+    internal RepeatNodeStart startNode;
 
     public HashSet<IReadOnlyNode> HandledNodes => CalculateHandledNodes();
 
@@ -38,7 +38,7 @@ public class RepeatNodeEnd : Node, IPairNode, IExecutionFlowNode
 
     protected override void OnExecute(RenderContext context)
     {
-        if (OtherNode == Guid.Empty)
+        if (startNode == null && OtherNode == Guid.Empty)
         {
             return;
         }
@@ -74,31 +74,40 @@ public class RepeatNodeEnd : Node, IPairNode, IExecutionFlowNode
         return new RepeatNodeEnd();
     }
 
-    private RepeatNodeStart FindStartNode()
+    internal RepeatNodeStart FindStartNode(out List<IReadOnlyNode> reversedCalculationQueue)
     {
         RepeatNodeStart startNode = null;
         int nestingCount = 0;
-        TraverseBackwards(node =>
+
+        var queue = GraphUtils.CalculateExecutionQueue(this);
+
+        int nestingLevel = 0;
+        var reversedQueue = queue.Reverse().ToList();
+        foreach (var node in reversedQueue)
         {
-            if (node is RepeatNodeEnd && node != this)
+            if (node == this)
             {
-                nestingCount++;
+                continue;
             }
 
-            if (node is RepeatNodeStart leftNode && nestingCount == 0)
+            if (node is RepeatNodeEnd)
             {
-                startNode = leftNode;
-                return false;
+                nestingLevel++;
             }
 
-            if (node is RepeatNodeStart)
+            if (node is RepeatNodeStart leftNode)
             {
-                nestingCount--;
-            }
+                if (nestingLevel == 0)
+                {
+                    startNode = leftNode;
+                    break;
+                }
 
-            return true;
-        });
+                nestingLevel--;
+            }
+        }
 
+        reversedCalculationQueue = reversedQueue;
         return startNode;
     }
 
@@ -106,29 +115,40 @@ public class RepeatNodeEnd : Node, IPairNode, IExecutionFlowNode
     {
         HashSet<IReadOnlyNode> handled = new();
 
-        startNode = FindStartNode();
+        startNode = FindStartNode(out var calculationQueue);
 
         int nestingCount = 0;
-        var queue = GraphUtils.CalculateExecutionQueue(this, false, property => property.Connection.Node != startNode);
 
-        foreach (var node in queue)
+        bool withinPair = false;
+        foreach (var node in calculationQueue)
         {
-            if (node is RepeatNodeStart && node != this)
+            if (node == this)
+            {
+                withinPair = true;
+                continue;
+            }
+
+            if (node is RepeatNodeEnd)
             {
                 nestingCount++;
             }
 
-            if (node is RepeatNodeEnd leftNode && nestingCount == 0)
+            if (node is RepeatNodeStart leftNode)
             {
-                if (leftNode == this)
+                if (nestingCount == 0)
                 {
-                    break;
+                    if (leftNode == startNode)
+                    {
+                        break;
+                    }
+                }
+                else
+                {
+                    nestingCount--;
                 }
-
-                nestingCount--;
             }
 
-            if (node != this && node != startNode)
+            if (withinPair)
             {
                 handled.Add(node);
             }

+ 182 - 79
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/RepeatNodeStart.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.Shaders.Generation;
+using System.Diagnostics;
+using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -19,13 +20,16 @@ public class RepeatNodeStart : Node, IPairNode
     private RepeatNodeEnd? endNode;
 
     private bool iterationInProgress = false;
-    private Guid virtualSessionId;
     private Queue<IReadOnlyNode> unrolledQueue;
     private List<IReadOnlyNode> clonedNodes = new List<IReadOnlyNode>();
 
+    private Queue<IReadOnlyNode> cachedExecutionQueue;
+    private int lastHash = 0;
+
     public RepeatNodeStart()
     {
-        Iterations = CreateInput<int>("Iterations", "ITERATIONS", 1);
+        Iterations = CreateInput<int>("Iterations", "ITERATIONS", 1)
+            .WithRules(x => x.Min(0));
         Input = CreateInput<object>("Input", "INPUT", null);
         CurrentIteration = CreateOutput<int>("CurrentIteration", "CURRENT_ITERATION", 1);
         Output = CreateOutput<object>("Output", "OUTPUT", null);
@@ -33,53 +37,92 @@ public class RepeatNodeStart : Node, IPairNode
 
     protected override void OnExecute(RenderContext context)
     {
-        endNode = FindEndNode();
-        if (endNode == null)
+        try
         {
-            return;
-        }
-
-        OtherNode = endNode?.Id ?? Guid.Empty;
+            if (iterationInProgress)
+            {
+                return;
+            }
 
-        int iterations = Iterations.Value;
-        var queue = GraphUtils.CalculateExecutionQueue(endNode, true, true,
-            property => property.Connection?.Node != this);
+            iterationInProgress = true;
 
-        if (iterationInProgress)
-        {
-            return;
-        }
+            endNode = FindEndNode();
+            if (endNode == null)
+            {
+                return;
+            }
 
-        iterationInProgress = true;
+            OtherNode = endNode?.Id ?? Guid.Empty;
+            endNode.startNode = this;
+            int iterations = Iterations.Value;
 
-        if (iterations == 0)
-        {
-            Output.Value = null;
             CurrentIteration.Value = 0;
-            iterationInProgress = false;
-            return;
-        }
 
-        if (iterations > 1)
+            if (iterations <= 0)
+            {
+                Output.Value = null;
+                iterationInProgress = false;
+                return;
+            }
+
+            if (iterations > 1)
+            {
+                /*var unrollQueue = GraphUtils.CalculateExecutionQueue(endNode, false, true,
+                    property => property.Connection?.Node != this);*/
+                var unrollQueue = endNode.HandledNodes;
+
+
+                //int currentHash = GetGraphCache(unrollQueue);
+                //if (cachedExecutionQueue == null || lastHash != currentHash)
+                {
+                    ClearLastUnrolledNodes();
+
+                    context.BeginVirtualConnectionScope(context.ContextVirtualSession);
+                    cachedExecutionQueue =
+                        UnrollLoop(iterations, unrollQueue, context, context.ContextVirtualSession);
+
+
+                    //lastHash = currentHash;
+                }
+            }
+            else
+            {
+                cachedExecutionQueue = GraphUtils.CalculateExecutionQueue(endNode, true, true,
+                    property => property.Connection?.Node != this);
+            }
+
+            CurrentIteration.Value = 1;
+            Output.Value = Input.Value;
+
+            foreach (var node in cachedExecutionQueue)
+            {
+                context.SetActiveVirtualConnectionScope(context.ContextVirtualSession);
+                node.Execute(context);
+                if (node is RepeatNodeEnd)
+                {
+                    CurrentIteration.Value = Math.Min(CurrentIteration.Value + 1, iterations);
+                }
+            }
+        }
+        catch (Exception ex)
         {
-            ClearLastUnrolledNodes();
-            virtualSessionId = Guid.NewGuid();
-            context.BeginVirtualConnectionScope(virtualSessionId);
-            var unrollQueue = GraphUtils.CalculateExecutionQueue(endNode, false, true,
-                property => property.Connection?.Node != this);
-            queue = UnrollLoop(iterations, unrollQueue, context);
+            iterationInProgress = false;
+            throw;
         }
 
-        CurrentIteration.Value = 1;
-        Output.Value = Input.Value;
+        iterationInProgress = false;
+    }
 
-        foreach (var node in queue)
+    private int GetGraphCache(Queue<IReadOnlyNode> standardGraph)
+    {
+        HashCode hash = new HashCode();
+        if (standardGraph == null) return 0;
+        foreach (var node in standardGraph)
         {
-            context.SetActiveVirtualConnectionScope(virtualSessionId);
-            node.Execute(context);
+            hash.Add(node.GetCacheHash());
         }
 
-        iterationInProgress = false;
+        return hash.ToHashCode();
     }
 
     private void ClearLastUnrolledNodes()
@@ -95,36 +138,110 @@ public class RepeatNodeStart : Node, IPairNode
         }
     }
 
-    private Queue<IReadOnlyNode> UnrollLoop(int iterations, Queue<IReadOnlyNode> executionQueue, RenderContext context)
+    internal Queue<IReadOnlyNode> UnrollLoop(int iterations, HashSet<IReadOnlyNode> executionQueue,
+        RenderContext context,
+        Guid virtualSession)
     {
+        if (endNode == null)
+        {
+            endNode = FindEndNode();
+            if (endNode == null)
+            {
+                return new Queue<IReadOnlyNode>();
+            }
+        }
+
         var connectToNextStart = endNode.Input.Connection;
         var connectPreviousTo = Output.Connections;
-        var originalConnectedToIteration = CurrentIteration.Connections;
+        var originalConnectedToIteration = new List<IInputProperty>(CurrentIteration.Connections);
+
+        foreach (var input in CurrentIteration.Connections)
+        {
+            if (input.Connection != null)
+            {
+                input.SetVirtualNonOverridenValue(1, context.ContextVirtualSession, context);
+            }
+        }
 
-        Queue<IReadOnlyNode> lastQueue = new Queue<IReadOnlyNode>(executionQueue.Where(x => x != this && x != endNode));
+        HashSet<IReadOnlyNode> lastQueue = executionQueue;
+        Dictionary<Guid, Guid> originalIdMappings = new Dictionary<Guid, Guid>();
         for (int i = 0; i < iterations - 1; i++)
         {
             var mapping = new Dictionary<Guid, Node>();
-            CloneNodes(lastQueue, mapping, virtualSessionId, context, i + 2);
+            CloneNodes(lastQueue, mapping, originalIdMappings);
+
             connectPreviousTo =
-                ReplaceConnections(connectToNextStart, connectPreviousTo, mapping, virtualSessionId, context);
+                ReplaceConnections(connectToNextStart, connectPreviousTo, mapping, context.ContextVirtualSession,
+                    context);
+
             connectToNextStart = mapping[connectToNextStart.Node.Id].OutputProperties
-                .FirstOrDefault(y => y.InternalPropertyName == connectToNextStart.InternalPropertyName);
+                .TryGetProperty(connectToNextStart.InternalPropertyName);
+
+            ConnectExternalConnectionsToClonedNodes(lastQueue, mapping, context, context.ContextVirtualSession, i + 2);
 
             originalConnectedToIteration =
-                SetIterationConstants(mapping, originalConnectedToIteration, i + 2, virtualSessionId, context);
+                SetIterationConstants(mapping, originalConnectedToIteration, i + 2, context.ContextVirtualSession,
+                    context);
 
             clonedNodes.AddRange(mapping.Values);
-            lastQueue = new Queue<IReadOnlyNode>(mapping.Values);
+            lastQueue = new HashSet<IReadOnlyNode>(mapping.Values);
         }
 
-        connectToNextStart.VirtualConnectTo(endNode.Input, virtualSessionId, context);
+
+        connectToNextStart.VirtualConnectTo(endNode.Input, context.ContextVirtualSession, context);
 
         var finalQueue = GraphUtils.CalculateExecutionQueue(endNode, true, true,
             property => property.Connection?.Node != this);
         return finalQueue;
     }
 
+    private void ConnectExternalConnectionsToClonedNodes(HashSet<IReadOnlyNode> originalQueue,
+        Dictionary<Guid, Node> mapping, RenderContext context, Guid virtualSession, int i1)
+    {
+        foreach (var node in originalQueue)
+        {
+            if (node is not Node n) continue;
+            if (!mapping.TryGetValue(node.Id, out var clonedNode))
+            {
+                continue;
+            }
+
+            foreach (var input in n.InputProperties)
+            {
+                if (input.Connection != null)
+                {
+                    var clonedInput =
+                        clonedNode.InputProperties.FirstOrDefault(i =>
+                            i.InternalPropertyName == input.InternalPropertyName);
+                    if (clonedInput is { Connection: null } && input.Connection != Output &&
+                        !mapping.TryGetValue(input.Connection.Node.Id, out _))
+                    {
+                        if (input.Connection.InternalPropertyName == CurrentIteration.InternalPropertyName)
+                        {
+                            if (input.Connection == CurrentIteration)
+                            {
+                                var iteration = i1;
+                                clonedInput.SetVirtualNonOverridenValue(iteration, virtualSession, context);
+                            }
+                            else if (input.Connection.Node is RepeatNodeStart start)
+                            {
+                                //start.CurrentIteration.VirtualConnectTo(clonedInput, virtualSession, context);
+                            }
+
+                        }
+                        /*if (input.Connection == CurrentIteration)
+                        {
+                            clonedInput.SetVirtualNonOverridenValue(i1, virtualSession, context);
+                            continue;
+                        }*/
+
+                        //input.Connection.VirtualConnectTo(clonedInput, virtualSession, context);
+                    }
+                }
+            }
+        }
+    }
+
     private List<IInputProperty> SetIterationConstants(Dictionary<Guid, Node> mapping,
         IReadOnlyCollection<IInputProperty> originalConnectedToIteration, int iteration, Guid virtualConnectionId,
         RenderContext context)
@@ -135,8 +252,7 @@ public class RepeatNodeStart : Node, IPairNode
             if (mapping.TryGetValue(input.Node.Id, out var mappedNode))
             {
                 var mappedInput =
-                    mappedNode.InputProperties.FirstOrDefault(i =>
-                        i.InternalPropertyName == input.InternalPropertyName);
+                    mappedNode.InputProperties.TryGetProperty(input.InternalPropertyName);
 
                 if (mappedInput == null) continue;
                 mappedInput.SetVirtualNonOverridenValue(iteration, virtualConnectionId, context);
@@ -157,8 +273,7 @@ public class RepeatNodeStart : Node, IPairNode
             if (mapping.TryGetValue(input.Node.Id, out var mappedNode))
             {
                 var mappedInput =
-                    mappedNode.InputProperties.FirstOrDefault(i =>
-                        i.InternalPropertyName == input.InternalPropertyName);
+                    mappedNode.InputProperties.TryGetProperty(input.InternalPropertyName);
                 if (mappedInput != null)
                 {
                     connectPreviousToMapped.Add(mappedInput);
@@ -174,61 +289,49 @@ public class RepeatNodeStart : Node, IPairNode
         return connectPreviousToMapped;
     }
 
-    private void CloneNodes(Queue<IReadOnlyNode> originalQueue, Dictionary<Guid, Node> mapping, Guid virtualSession,
-        RenderContext context, int i)
+    private void CloneNodes(HashSet<IReadOnlyNode> originalQueue, Dictionary<Guid, Node> mapping,
+        Dictionary<Guid, Guid> originalIdMappings)
     {
         foreach (var node in originalQueue)
         {
             if (node is not Node n) continue;
-            Node clonedNode;
-            clonedNode = n.Clone();
+            if (node == this || node == endNode || endNode.HandledNodes.All(x =>
+                    x.Id != node.Id && x.Id != originalIdMappings.GetValueOrDefault(node.Id))) continue;
+            var clonedNode = n.Clone();
 
             mapping[node.Id] = clonedNode;
+            Guid originalId = originalIdMappings.ContainsKey(node.Id)
+                ? originalIdMappings[node.Id]
+                : node.Id;
+            originalIdMappings[clonedNode.Id] = originalId;
         }
 
-        ConnectRelatedNodes(originalQueue, mapping, virtualSession, context, i);
+        ConnectRelatedNodes(originalQueue, mapping);
     }
 
-    private void ConnectRelatedNodes(Queue<IReadOnlyNode> originalQueue, Dictionary<Guid, Node> mapping,
-        Guid virtualSession, RenderContext context, int i1)
+    private void ConnectRelatedNodes(HashSet<IReadOnlyNode> originalQueue, Dictionary<Guid, Node> mapping)
     {
         foreach (var node in originalQueue)
         {
             if (node is not Node n) continue;
-            var clonedNode = mapping[node.Id];
+            if (!mapping.TryGetValue(node.Id, out var clonedNode))
+            {
+                continue;
+            }
 
             foreach (var input in n.InputProperties)
             {
                 if (input.Connection != null &&
                     mapping.TryGetValue(input.Connection.Node.Id, out var connectedClonedNode))
                 {
-                    var output = connectedClonedNode.OutputProperties.FirstOrDefault(o =>
-                        o.InternalPropertyName == input.Connection.InternalPropertyName);
+                    var output =
+                        connectedClonedNode.OutputProperties.TryGetProperty(input.Connection.InternalPropertyName);
                     if (output != null)
                     {
-                        var inputProp = clonedNode.InputProperties.FirstOrDefault(i =>
-                            i.InternalPropertyName == input.InternalPropertyName);
+                        var inputProp = clonedNode.InputProperties.TryGetProperty(input.InternalPropertyName);
                         output.ConnectTo(inputProp); // No need for virtual connection as it is a cloned node anyway
                     }
                 }
-                // Leaving this in case external connections are not working as intended. It might help, but no guarantees.
-                /*else if (input.Connection != null)
-                {
-                    var clonedInput =
-                        clonedNode.InputProperties.FirstOrDefault(i =>
-                            i.InternalPropertyName == input.InternalPropertyName);
-                    if (clonedInput is { Connection: null } && input.Connection != Output &&
-                        !mapping.TryGetValue(input.Connection.Node.Id, out _))
-                    {
-                        if (input.Connection == CurrentIteration)
-                        {
-                            clonedInput.SetVirtualNonOverridenValue(i1, virtualSession, context);
-                            continue;
-                        }
-
-                        input.Connection.VirtualConnectTo(clonedInput, virtualSession, context);
-                    }
-                }*/
             }
         }
     }

+ 52 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/OutputProperties.cs

@@ -0,0 +1,52 @@
+using System.Collections;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public class OutputProperties : IReadOnlyOutputProperties
+{
+    public Dictionary<string, OutputProperty> Properties { get; } = new();
+
+    public void Add(OutputProperty property)
+    {
+        if (!Properties.TryAdd(property.InternalPropertyName, property))
+            throw new ArgumentException(
+                $"Property with name {property.InternalPropertyName} already exists in this collection.");
+    }
+
+    public void Add<T>(OutputProperty<T> property)
+    {
+        if (!Properties.TryAdd(property.InternalPropertyName, property))
+            throw new ArgumentException(
+                $"Property with name {property.InternalPropertyName} already exists in this collection.");
+    }
+
+    public IEnumerator<IOutputProperty> GetEnumerator()
+    {
+        return Properties.Values.GetEnumerator();
+    }
+
+    public OutputProperty? TryGetProperty(string propertyName)
+    {
+        Properties.TryGetValue(propertyName, out var prop);
+        return prop;
+    }
+
+    public int Count => Properties.Count;
+
+    IOutputProperty? IReadOnlyOutputProperties.TryGetProperty(string propertyName)
+    {
+        return TryGetProperty(propertyName);
+    }
+
+    IEnumerator IEnumerable.GetEnumerator()
+    {
+        return GetEnumerator();
+    }
+}
+
+public interface IReadOnlyOutputProperties : IEnumerable<IOutputProperty>
+{
+    public IOutputProperty? TryGetProperty(string propertyName);
+    int Count { get; }
+}

+ 13 - 4
src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs

@@ -29,6 +29,8 @@ public class RenderContext
     private Dictionary<Guid, List<InputProperty>> recordedVirtualInputs = new();
     private Dictionary<Guid, List<OutputProperty>> recordedVirtualOutputs = new();
 
+    public Guid ContextVirtualSession { get; } = Guid.NewGuid();
+
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
         VecI renderOutputSize, VecI documentSize, ColorSpace processingColorSpace, SamplingOptions desiredSampling, double opacity = 1)
     {
@@ -109,19 +111,26 @@ public class RenderContext
         }
     }
 
-    public void EndVirtualConnectionScope(Guid virtualSessionId)
+    public void EndVirtualConnectionScope(Guid virtualSessionId, bool removeConnections = true)
     {
         if (!virtualGraphSessions.Contains(virtualSessionId))
             return;
 
-        virtualGraphSessions.Remove(virtualSessionId);
+        if (removeConnections)
+        {
+            virtualGraphSessions.Remove(virtualSessionId);
+        }
 
         foreach (var inputProperty in recordedVirtualInputs)
         {
             foreach (var input in inputProperty.Value)
             {
-                input.RemoveVirtualConnection(virtualSessionId);
-                input.RemoveVirtualNonOverridenValues(virtualSessionId);
+                if (removeConnections)
+                {
+                    input.RemoveVirtualConnection(virtualSessionId);
+                    input.RemoveVirtualNonOverridenValues(virtualSessionId);
+                }
+
                 input.ActiveVirtualSession = null;
             }
         }

+ 5 - 1
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -69,6 +69,9 @@ internal static class PixiParserDocumentEx
         Dictionary<string, object> dict = new();
         foreach (var property in properties)
         {
+            if (property == null)
+                continue;
+
             dict[property.PropertyName] = property.Value;
         }
 
@@ -80,7 +83,8 @@ internal static class PixiParserDocumentEx
         DocumentViewModelBuilder.ReferenceLayerBuilder layerBuilder,
         ImageEncoder encoder)
     {
-        var surface = DecodeSurface(referenceLayer.ImageBytes, referenceLayer.ImageWidth, referenceLayer.ImageHeight, encoder);
+        var surface = DecodeSurface(referenceLayer.ImageBytes, referenceLayer.ImageWidth, referenceLayer.ImageHeight,
+            encoder);
 
         layerBuilder
             .WithIsVisible(referenceLayer.Enabled)

+ 7 - 4
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -478,14 +478,17 @@ internal partial class DocumentViewModel
         {
             NodePropertyValue[] properties = new NodePropertyValue[node.InputProperties.Count];
 
-            for (int i = 0; i < node.InputProperties.Count(); i++)
+            int index = 0;
+            foreach(var input in node.InputProperties)
             {
-                properties[i] = new NodePropertyValue()
+                properties[index] = new NodePropertyValue()
                 {
-                    PropertyName = node.InputProperties[i].InternalPropertyName,
-                    Value = SerializationUtil.SerializeObject(node.InputProperties[i].NonOverridenValue, config,
+                    PropertyName = input.InternalPropertyName,
+                    Value = SerializationUtil.SerializeObject(input.NonOverridenValue, config,
                         allFactories)
                 };
+
+                index++;
             }
 
             Dictionary<string, object> additionalData = new();

+ 407 - 0
tests/PixiEditor.Backend.Tests/RepeatNodeTests.cs

@@ -0,0 +1,407 @@
+using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Utility;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.Tests;
+
+namespace PixiEditor.Backend.Tests;
+
+public class RepeatNodeTests : PixiEditorTest
+{
+    [Fact]
+    public void TestThatRepeatNodeProperlyDuplicatesNodes()
+    {
+        RepeatNodeStart start = new();
+        RepeatNodeEnd end = new();
+        MathNode mathNode = new();
+
+        mathNode.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+        start.Output.ConnectTo(mathNode.Y);
+
+        mathNode.Result.ConnectTo(end.Input);
+        start.Iterations.NonOverridenValue = 3;
+
+        RenderContext context = new(null, new KeyFrameTime(), ChunkResolution.Full, VecI.Zero, VecI.Zero,
+            ColorSpace.CreateSrgb(), SamplingOptions.Bilinear);
+        var executionQueue = GraphUtils.CalculateExecutionQueue(end, true);
+        foreach (var node in executionQueue)
+        {
+            node.Execute(context);
+        }
+
+        Assert.True(end.Output.Value is Delegate);
+        var func = (Delegate)end.Output.Value!;
+        var result = func.DynamicInvoke(ShaderFuncContext.NoContext);
+        Assert.Equal(3, ((Float1)result!).ConstantValue);
+    }
+
+    [Fact]
+    public void TestThatNestedRepeatNodesProperlyExecutes()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+
+        mathNode.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+        startInner.Output.ConnectTo(mathNode.Y);
+        mathNode.Result.ConnectTo(endInner.Input);
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        startOuter.Iterations.NonOverridenValue = 2;
+        startInner.Iterations.NonOverridenValue = 7;
+
+        RenderContext context = new(null, new KeyFrameTime(), ChunkResolution.Full, VecI.Zero, VecI.Zero,
+            ColorSpace.CreateSrgb(), SamplingOptions.Bilinear);
+        var executionQueue = GraphUtils.CalculateExecutionQueue(endOuter, true);
+        foreach (var node in executionQueue)
+        {
+            node.Execute(context);
+        }
+
+        Assert.True(endOuter.Output.Value is Delegate);
+        var func = (Delegate)endOuter.Output.Value!;
+        var result = func.DynamicInvoke(ShaderFuncContext.NoContext);
+        Assert.Equal(14, ((Float1)result!).ConstantValue);
+    }
+
+    [Fact]
+    public void TestThatNodePairingWorksForMultipleNestingLevels()
+    {
+        RepeatNodeStart start1 = new();
+        RepeatNodeEnd end1 = new();
+
+        RepeatNodeStart start2 = new();
+        RepeatNodeEnd end2 = new();
+
+        start1.Output.ConnectTo(start2.Input);
+        start2.Output.ConnectTo(end2.Input);
+        end2.Output.ConnectTo(end1.Input);
+
+        var emptyContext = new RenderContext(null, new KeyFrameTime(), ChunkResolution.Full, VecI.Zero, VecI.Zero,
+            ColorSpace.CreateSrgb(), SamplingOptions.Bilinear);
+        start1.Execute(emptyContext);
+        start2.Execute(emptyContext);
+        end2.Execute(emptyContext);
+        end1.Execute(emptyContext);
+
+        Assert.Equal(start1.OtherNode, end1.Id);
+        Assert.Equal(end1.OtherNode, start1.Id);
+        Assert.Equal(start2.OtherNode, end2.Id);
+        Assert.Equal(end2.OtherNode, start2.Id);
+    }
+
+    [Fact]
+    public void CalculateHandledNodesProperlyReportsMathNodeOnly()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+
+        mathNode.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+        startInner.Output.ConnectTo(mathNode.Y);
+        mathNode.Result.ConnectTo(endInner.Input);
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Single(endInner.HandledNodes);
+        Assert.Contains(mathNode, endOuter.HandledNodes);
+
+        Assert.Equal(3, endOuter.HandledNodes.Count); // mathNode, startInner, endInner
+        Assert.Contains(startInner, endOuter.HandledNodes);
+        Assert.Contains(endInner, endOuter.HandledNodes);
+    }
+
+    [Fact]
+    public void FindStartNodeProperlyFindsNodeInNestedZone()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        startInner.Output.ConnectTo(mathNode.Y);
+        mathNode.Result.ConnectTo(endInner.Input);
+
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Equal(startOuter, endOuter.FindStartNode(out _));
+        Assert.Equal(startInner, endInner.FindStartNode(out _));
+    }
+
+
+    [Fact]
+    public void FindStartNodeProperlyFindsNodeInNestedZoneWithOuterZoneReference()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+
+        startInner.Output.ConnectTo(mathNode.Y);
+        mathNode.Result.ConnectTo(endInner.Input);
+        startOuter.CurrentIteration.ConnectTo(mathNode.X);
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Equal(startOuter, endOuter.FindStartNode(out _));
+        Assert.Equal(startInner, endInner.FindStartNode(out _));
+    }
+
+    [Fact]
+    public void FindStartNodeProperlyFindsNodeInNestedZoneWithOuterZoneReference2()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+        MathNode mathNode2 = new();
+
+        startInner.Output.ConnectTo(mathNode.Y);
+        startInner.CurrentIteration.ConnectTo(mathNode2.X);
+        startOuter.CurrentIteration.ConnectTo(mathNode.Y);
+        mathNode.Result.ConnectTo(mathNode2.Y);
+
+        mathNode2.Result.ConnectTo(endInner.Input);
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Equal(startOuter, endOuter.FindStartNode(out _));
+        Assert.Equal(startInner, endInner.FindStartNode(out _));
+    }
+
+    [Fact]
+    public void TestThatNestedRepeatReferencingOuterActiveIterationCalculatesHandledNodesProperly2()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+        MathNode mathNode2 = new();
+
+        startInner.Output.ConnectTo(mathNode.Y);
+        startInner.CurrentIteration.ConnectTo(mathNode2.X);
+        startOuter.CurrentIteration.ConnectTo(mathNode.Y);
+        mathNode.Result.ConnectTo(mathNode2.Y);
+
+        mathNode2.Result.ConnectTo(endInner.Input);
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Equal(2, endInner.HandledNodes.Count);
+        Assert.Contains(mathNode, endOuter.HandledNodes);
+        Assert.Contains(mathNode2, endOuter.HandledNodes);
+
+        Assert.Equal(4, endOuter.HandledNodes.Count);
+        Assert.Contains(startInner, endOuter.HandledNodes);
+        Assert.Contains(endInner, endOuter.HandledNodes);
+        Assert.Contains(mathNode, endOuter.HandledNodes);
+        Assert.Contains(mathNode2, endOuter.HandledNodes);
+    }
+
+    [Fact]
+    public void TestThatNestedRepeatReferencingOuterActiveIterationCalculatesHandledNodesProperly()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        startOuter.CurrentIteration.ConnectTo(mathNode.X);
+        startInner.Output.ConnectTo(mathNode.Y);
+
+        mathNode.Result.ConnectTo(endInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Single(endInner.HandledNodes);
+        Assert.Contains(mathNode, endOuter.HandledNodes);
+
+        Assert.Equal(3, endOuter.HandledNodes.Count); // mathNode, startInner, endInner
+        Assert.Contains(startInner, endOuter.HandledNodes);
+        Assert.Contains(endInner, endOuter.HandledNodes);
+    }
+
+    [Fact]
+    public void TestThatStartNodesAreCalculatedProperlyForNestedRepeats()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNodeToConnectToOuter = new();
+        MathNode mathNodeToConnectToInner = new();
+        MathNode mathNodeToCombineBoth = new();
+
+        mathNodeToConnectToOuter.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+        mathNodeToConnectToInner.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        startOuter.CurrentIteration.ConnectTo(mathNodeToConnectToOuter.Y);
+        startInner.CurrentIteration.ConnectTo(mathNodeToConnectToInner.Y);
+        mathNodeToConnectToOuter.Result.ConnectTo(mathNodeToCombineBoth.X);
+        mathNodeToConnectToInner.Result.ConnectTo(mathNodeToCombineBoth.Y);
+        mathNodeToCombineBoth.Result.ConnectTo(endInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Equal(startOuter, endOuter.FindStartNode(out _));
+        Assert.Equal(startInner, endInner.FindStartNode(out _));
+    }
+
+    [Fact]
+    public void TestThatStartNodesAreCalculatedProperlyForUnrolledRepeatNodes()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNodeToConnectToOuter = new();
+        MathNode mathNodeToConnectToInner = new();
+        MathNode mathNodeToCombineBoth = new();
+
+        mathNodeToConnectToOuter.X.NonOverridenValue = context => new Float1("") { ConstantValue = 2 };
+        mathNodeToConnectToInner.X.NonOverridenValue = context => new Float1("") { ConstantValue = 2 };
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        startOuter.CurrentIteration.ConnectTo(mathNodeToConnectToOuter.Y);
+        startInner.CurrentIteration.ConnectTo(mathNodeToConnectToInner.Y);
+        mathNodeToConnectToOuter.Result.ConnectTo(mathNodeToCombineBoth.X);
+        mathNodeToConnectToInner.Result.ConnectTo(mathNodeToCombineBoth.Y);
+        mathNodeToCombineBoth.Result.ConnectTo(endInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        var emptyContext = new RenderContext(null, new KeyFrameTime(), ChunkResolution.Full, VecI.Zero, VecI.Zero,
+            ColorSpace.CreateSrgb(), SamplingOptions.Bilinear);
+
+        var unrollQueue = endOuter.HandledNodes;
+
+        var queue = startOuter.UnrollLoop(2, unrollQueue, emptyContext, emptyContext.ContextVirtualSession);
+
+        var startNode = queue.OfType<RepeatNodeStart>().First();
+        var endNode = queue.OfType<RepeatNodeEnd>().First();
+
+        foreach (var node in queue)
+        {
+            emptyContext.SetActiveVirtualConnectionScope(emptyContext.ContextVirtualSession);
+            node.Execute(emptyContext);
+        }
+
+        Assert.Equal(startNode.OtherNode, endNode.Id);
+        Assert.Equal(endNode.OtherNode, startNode.Id);
+        Assert.Equal(startNode.Id, endNode.OtherNode);
+        Assert.Equal(endNode.Id, startNode.OtherNode);
+
+        var queueInner = GraphUtils.CalculateExecutionQueue(endInner, false, true,
+            property => property.Connection?.Node != startInner);
+        var startNodeOuter = queueInner.OfType<RepeatNodeStart>().First();
+
+        Assert.Equal(endOuter.Id, startNodeOuter.OtherNode);
+    }
+
+    [Fact]
+    public void TestThatHandledNodesAreCalculatedProperlyForNestedRepeats()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNodeToConnectToOuter = new();
+        MathNode mathNodeToConnectToInner = new();
+        MathNode mathNodeToCombineBoth = new();
+
+        mathNodeToConnectToOuter.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+        mathNodeToConnectToInner.X.NonOverridenValue = context => new Float1("") { ConstantValue = 1 };
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        startOuter.CurrentIteration.ConnectTo(mathNodeToConnectToOuter.Y);
+        startInner.CurrentIteration.ConnectTo(mathNodeToConnectToInner.Y);
+        mathNodeToConnectToOuter.Result.ConnectTo(mathNodeToCombineBoth.X);
+        mathNodeToConnectToInner.Result.ConnectTo(mathNodeToCombineBoth.Y);
+        mathNodeToCombineBoth.Result.ConnectTo(endInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        Assert.Equal(2, endInner.HandledNodes.Count);
+        Assert.Contains(mathNodeToConnectToInner, endInner.HandledNodes);
+        Assert.Contains(mathNodeToCombineBoth, endInner.HandledNodes);
+
+        Assert.Equal(5, endOuter.HandledNodes.Count);
+        Assert.Contains(startInner, endOuter.HandledNodes);
+        Assert.Contains(endInner, endOuter.HandledNodes);
+        Assert.Contains(mathNodeToConnectToOuter, endOuter.HandledNodes);
+        Assert.Contains(mathNodeToConnectToInner, endOuter.HandledNodes);
+        Assert.Contains(mathNodeToCombineBoth, endOuter.HandledNodes);
+    }
+
+    [Fact]
+    public void TestThatNestedRepeatReferencingOuterActiveIterationCalculatesValueProperly()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode mathNode = new();
+
+        startOuter.Output.ConnectTo(startInner.Input);
+        startOuter.CurrentIteration.ConnectTo(mathNode.X);
+        startInner.Output.ConnectTo(mathNode.Y);
+
+        mathNode.Result.ConnectTo(endInner.Input);
+        endInner.Output.ConnectTo(endOuter.Input);
+
+        startOuter.Iterations.NonOverridenValue = 2;
+        startInner.Iterations.NonOverridenValue = 10;
+
+        RenderContext context = new(null, new KeyFrameTime(), ChunkResolution.Full, VecI.Zero, VecI.Zero,
+            ColorSpace.CreateSrgb(), SamplingOptions.Bilinear);
+        var executionQueue = GraphUtils.CalculateExecutionQueue(endOuter, true);
+
+        Assert.Equal(startOuter.Id, executionQueue.ElementAt(0).Id);
+        Assert.Equal(endOuter.Id, executionQueue.ElementAt(1).Id);
+
+        foreach (var node in executionQueue)
+        {
+            node.Execute(context);
+        }
+
+        Assert.True(endOuter.Output.Value is Delegate);
+        var func = (Delegate)endOuter.Output.Value!;
+        var result = func.DynamicInvoke(ShaderFuncContext.NoContext);
+        Assert.Equal(30, ((Float1)result!).ConstantValue);
+    }
+
+    [Fact]
+    public void TestThatNestedCurrentIterationUnrollsProperly()
+    {
+        RepeatNodeStart startOuter = new();
+        RepeatNodeEnd endOuter = new();
+        RepeatNodeStart startInner = new();
+        RepeatNodeEnd endInner = new();
+        MathNode toConnectToOuter = new();
+        MathNode toConnectToInner = new();
+        CombineVecDNode toCombineBoth = new();
+
+    }
+}