Explorar el Código

Fix Dependency Cycles Slowing Mantis Down

The new tree execution sorting is based on Kahn's algorithm which
prevents cycles from stopping the tree from generating. This is a big
usability improvement. I haven't tested it much yet, though. I need to
make some new tools for testing to proceed with this.

This commit is also a small ~0.005 speedup which scales with tree size.
I tested the performance on my Elephant scene.
Joseph Brandenburg hace 1 semana
padre
commit
8ecef2d3f8
Se han modificado 3 ficheros con 28 adiciones y 57 borrados
  1. 2 1
      base_definitions.py
  2. 1 0
      misc_nodes.py
  3. 25 56
      readtree.py

+ 2 - 1
base_definitions.py

@@ -714,9 +714,10 @@ class MantisNode:
         self.node_type='UNINITIALIZED'
         self.hierarchy_connections, self.connections = [], []
         self.hierarchy_dependencies, self.dependencies = [], []
+        self.unmet_hierarchy_dependency_count = float("inf") # we will set this when filling the dependencies lists
         self.prepared, self.executed = False, False
         self.execution_prepared = False
-        # the above is for tracking prep state in execution, so that I can avoid preparing nodes
+        # execution_prepared is for tracking prep state in execution, so that I can avoid preparing nodes
         #  again without changing the readtree code much.
         self.socket_templates = socket_templates
         self.mContext = None # for now I am gonna set this at runtime

+ 1 - 0
misc_nodes.py

@@ -1379,6 +1379,7 @@ class InputWidget(MantisNode):
                 from .geometry_node_graphgen import gen_simple_flip_modifier
                 ng = gen_simple_flip_modifier()
             flip_modifier.node_group = ng
+            # this changed at some point in Blender's versions ehhh
             flip_modifier["Socket_2"]=axes_flipped[0]
             flip_modifier["Socket_3"]=axes_flipped[1]
             flip_modifier["Socket_4"]=axes_flipped[2]

+ 25 - 56
readtree.py

@@ -485,8 +485,6 @@ def parse_tree(base_tree, error_popups=False):
     kept_mantis_node = {}
     while (all_mantis_nodes):
         mantis_node = all_mantis_nodes.pop()
-        if mantis_node in array_nodes:
-            continue
         if mantis_node.node_type in ["DUMMY", 'SCHEMA', 'DUMMY_SCHEMA']:
             continue # screen out the ui_node schema nodes, group in/out, and group placeholders
         # cleanup autogen nodes
@@ -536,8 +534,6 @@ def execution_error_cleanup(node, exception, switch_objects = [], show_error=Fal
             base_tree = node.base_tree
             tree = base_tree
             try:
-                pass
-                space = context.space_data
                 for name in ui_sig[1:]:
                     for n in tree.nodes: n.select = False
                     n = tree.nodes[name]
@@ -558,56 +554,33 @@ def execution_error_cleanup(node, exception, switch_objects = [], show_error=Fal
     prRed(f"Error: {exception} in node {ui_sig}")
     return exception
 
-def sort_execution(nodes, xForm_pass):
-    execution_failed=False
-    sorted_nodes = []
+def sort_execution(nodes):
     from .node_common import GraphError
-    # check for cycles here by keeping track of the number of times a node has been visited.
-    visited={}
-    check_max_len=len(nodes)**2 # seems too high but safe. In a well-ordered graph, I guess this number should be less than the number of nodes.
-    max_iterations = len(nodes)**2
-    i = 0
+    from collections import deque
+    xForm_pass = deque()
+    execution_failed = False
+    sorted_nodes = []
+    # initialize the hierarchy dependency count. We'll use this to ensure there
+    # are no cycles (using Kahn's algorithm).
+    for mantis_node in nodes.values():
+        mantis_node.unmet_hierarchy_dependency_count = len(mantis_node.hierarchy_dependencies)
+        if mantis_node.unmet_hierarchy_dependency_count == 0 :
+            xForm_pass.append(mantis_node)
+    
+    processed_count = 0
     while(xForm_pass):
-        if execution_failed: break
-        if i >= max_iterations:
-            execution_failed = True
-            raise GraphError("There is probably a cycle somewhere in the graph. "
-                                "Or a connection missing in a Group/Schema Input")
-        i+=1
         n = xForm_pass.pop()
-        if visited.get(n.signature) is not None:
-            visited[n.signature]+=1
-        else:
-            visited[n.signature]=0
-        if visited[n.signature] > check_max_len:
-            execution_failed = True
-            raise GraphError("There is a probably a cycle in the graph somewhere. "
-                                "Or a connection missing in a Group/Schema Input")
-            # we're trying to solve the halting problem at this point.. don't do that.
-            # TODO find a better way! there are algo's for this but they will require using a different solving algo, too
-        if n.execution_prepared:
-            continue
-        if n.node_type not in ['XFORM', 'UTILITY']:
-            for dep in n.hierarchy_dependencies:
-                if not dep.execution_prepared:
-                    xForm_pass.appendleft(n) # hold it
-                    break
-            else:
-                n.execution_prepared=True
-                sorted_nodes.append(n)
-                for conn in n.hierarchy_connections:
-                    if  not conn.execution_prepared:
-                        xForm_pass.appendleft(conn)
-        else:
-            for dep in n.hierarchy_dependencies:
-                if not dep.execution_prepared:
-                    break
-            else:
-                n.execution_prepared=True
-                sorted_nodes.append(n)
-                for conn in n.hierarchy_connections:
-                    if  not conn.execution_prepared:
-                        xForm_pass.appendleft(conn)
+        # if n.execution_prepared: continue
+        n.execution_prepared = True
+        sorted_nodes.append(n)
+        processed_count+=1
+        for child in n.hierarchy_connections:
+            child.unmet_hierarchy_dependency_count-= 1
+            if child.unmet_hierarchy_dependency_count == 0:
+                xForm_pass.append(child)
+    if processed_count < len(nodes):
+        execution_failed = True
+        raise GraphError("detected a cycle in the tree")
     return sorted_nodes, execution_failed
 
 def execute_tree(nodes, base_tree, context, error_popups = False, profile=False):
@@ -616,18 +589,14 @@ def execute_tree(nodes, base_tree, context, error_popups = False, profile=False)
                            " Mantis probably failed to parse the tree."
     import bpy
     from time import time
-    from .node_common import GraphError
     original_active = context.view_layer.objects.active
     start_execution_time = time()
     mContext = None
-    from collections import deque
-    xForm_pass = deque()
     for mantis_node in nodes.values():
         if not mContext: # just grab one of these. this is a silly way to do this.
             mContext = mantis_node.mContext
             mContext.b_objects = {} # clear the objects and recreate them
         mantis_node.reset_execution()
-        check_and_add_root(mantis_node, xForm_pass)
     mContext.execution_failed = False
 
     select_me, switch_me = [], [] # switch the mode on these objects
@@ -640,7 +609,7 @@ def execute_tree(nodes, base_tree, context, error_popups = False, profile=False)
         profiler.new_session(mContext.execution_id)
         sort_key = 'total'
     try:
-        sorted_nodes, execution_failed = sort_execution(nodes, xForm_pass)
+        sorted_nodes, execution_failed = sort_execution(nodes)
         for n in sorted_nodes:
             try:
                 if not n.prepared: