44import copy
55import os
66import re
7+ import threading
78import weakref
89import logging
910import inspect
@@ -185,8 +186,17 @@ def _getValue(self):
185186 raise RuntimeError (f"Cannot get value of { self ._getFullName ()} , the attribute is keyable." )
186187 if self .isLink :
187188 return self ._getInputLink ().value
189+ self ._resolveValue ()
188190 return self ._value
189191
192+ def _resolveValue (self ):
193+ """
194+ Hook for subclasses to resolve pending values before returning _value.
195+ Called by _getValue before returning self._value.
196+ Default implementation is a no-op.
197+ """
198+ pass
199+
190200 def _setValue (self , value ):
191201 """
192202 Set the attribute value from a given value, a given function or a given attribute.
@@ -820,6 +830,9 @@ def getSerializedValue(self):
820830
821831class ListAttribute (Attribute ):
822832
833+ # Sentinel to distinguish 'no pending dynamic value' from 'pending reset to empty'
834+ _NO_PENDING_VALUE = object ()
835+
823836 def __init__ (self , node , attributeDesc : desc .ListAttribute , isOutput : bool ,
824837 root = None , parent = None ):
825838 super ().__init__ (node , attributeDesc , isOutput , root , parent )
@@ -861,13 +874,14 @@ def insert(self, index, value):
861874 self ._value .insert (index , attrs )
862875 self .valueChanged .emit ()
863876 self ._applyExpr ()
864- self .requestGraphUpdate ()
877+ if self .isInput :
878+ self .requestGraphUpdate ()
865879
866880 @raiseIfLink
867881 def remove (self , index , count = 1 ):
868882 if self ._value is None :
869883 return
870- if self .node .graph :
884+ if self .node .graph and self . isInput :
871885 from meshroom .core .graph import GraphModification
872886 with GraphModification (self .node .graph ):
873887 # remove potential links
@@ -877,15 +891,41 @@ def remove(self, index, count=1):
877891 # delete edge if the attribute is linked
878892 self .node .graph .removeEdge (attr )
879893 self ._value .removeAt (index , count )
880- self .requestGraphUpdate ()
894+ if self .isInput :
895+ self .requestGraphUpdate ()
881896 self .valueChanged .emit ()
882897
883898 # Override
884899 def _initValue (self ):
900+ self ._dynamicValueLock = threading .Lock ()
901+ self ._dynamicValue = ListAttribute ._NO_PENDING_VALUE
885902 self .resetToDefaultValue ()
886903
887904 # Override
888905 def _setValue (self , value ):
906+ if self .isOutput :
907+ # For output attributes (set during processChunk in a worker thread,
908+ # or during loadOutputAttr in the TaskThread), store raw values without
909+ # creating QObject children to avoid cross-thread parenting issues.
910+ # The raw values are:
911+ # - serialized by saveOutputAttr via getPrimitiveValue
912+ # - materialized into QObjects lazily by _getValue on the main thread
913+ with self ._dynamicValueLock :
914+ if value is None :
915+ self ._dynamicValue = None # pending reset
916+ else :
917+ self ._dynamicValue = self ._desc .validateValue (value )
918+ return
919+
920+ # Input attribute path: handle None
921+ if value is None :
922+ if self .node .graph and self ._value is not None and len (self ._value ) > 0 :
923+ self .remove (0 , len (self ))
924+ if self ._value is None :
925+ self ._value = ListModel (parent = self )
926+ self .valueChanged .emit ()
927+ return
928+
889929 if self .node .graph :
890930 self .remove (0 , len (self ))
891931 if self ._handleLinkValue (value ):
@@ -897,7 +937,8 @@ def _setValue(self, value):
897937 self ._value = ListModel (parent = self )
898938 newValue = self ._desc .validateValue (value )
899939 self .extend (newValue )
900- self .requestGraphUpdate ()
940+ if self .isInput :
941+ self .requestGraphUpdate ()
901942
902943 # Override
903944 def _applyExpr (self ):
@@ -907,6 +948,20 @@ def _applyExpr(self):
907948 for value in self ._value :
908949 value ._applyExpr ()
909950
951+ def _populateFromDynamicValue (self , value ):
952+ """Store raw dynamic values for lazy materialization.
953+
954+ Does NOT create QObject children — safe to call from any thread.
955+ The actual ListModel population happens lazily in _getValue()
956+ when the main thread (e.g. QML) reads the value.
957+ """
958+ with self ._dynamicValueLock :
959+ if value is None :
960+ self ._dynamicValue = None # pending reset
961+ else :
962+ self ._dynamicValue = self ._desc .validateValue (value )
963+ self .valueChanged .emit ()
964+
910965 # Override
911966 def resetToDefaultValue (self ):
912967 self ._value = ListModel (parent = self )
@@ -922,8 +977,57 @@ def getSerializedValue(self):
922977 return self ._getInputLink ().asLinkExpr ()
923978 return [attr .getSerializedValue () for attr in self ._value ]
924979
980+ value = Property (Variant , Attribute ._getValue , _setValue , notify = Attribute .valueChanged )
981+
982+ # Override
983+ def _resolveValue (self ):
984+ """
985+ Lazily materialize QObject children from pending raw dynamic values.
986+ Called by Attribute._getValue (base) before returning self._value.
987+ This hook dispatches via normal Python MRO, bypassing PySide Property
988+ getter dispatch limitations.
989+ Must only create QObjects on the main thread to avoid cross-thread issues.
990+ """
991+ if self ._dynamicValue is not ListAttribute ._NO_PENDING_VALUE :
992+ if threading .current_thread () is threading .main_thread ():
993+ self ._materializeDynamicValue ()
994+
995+ def _materializeDynamicValue (self ):
996+ """
997+ Create QObject children in the ListModel from pending raw dynamic values.
998+ Must only be called on the main thread.
999+ """
1000+
1001+ # Thread proof reading of dynamic value
1002+ with self ._dynamicValueLock :
1003+ pendingValue = self ._dynamicValue
1004+ self ._dynamicValue = ListAttribute ._NO_PENDING_VALUE
1005+
1006+ # Create an empty list if the value is None
1007+ if self ._value is None :
1008+ self ._value = ListModel (parent = self )
1009+ elif len (self ._value ) > 0 :
1010+ # Erase all items before reassigning
1011+ self ._value .removeAt (0 , len (self ._value ))
1012+
1013+ # Effectively create the objects from the raw data
1014+ if pendingValue :
1015+ attrs = [attributeFactory (self ._desc .elementDesc , v , self .isOutput , self .node , self )
1016+ for v in pendingValue ]
1017+ self ._value .insert (0 , attrs )
1018+
1019+ self .valueChanged .emit ()
1020+
9251021 # Override
9261022 def getPrimitiveValue (self , exportDefault = True ):
1023+ # If there is a pending dynamic value (set or reset), return it directly
1024+ # without touching the ListModel (which may be on a different thread).
1025+ with self ._dynamicValueLock :
1026+ if self ._dynamicValue is not ListAttribute ._NO_PENDING_VALUE :
1027+ if self ._dynamicValue is None :
1028+ return []
1029+ return list (self ._dynamicValue )
1030+
9271031 if exportDefault :
9281032 return [attr .getPrimitiveValue (exportDefault = exportDefault ) for attr in self ._value ]
9291033 return [attr .getPrimitiveValue (exportDefault = exportDefault ) for attr in self ._value
0 commit comments