44import copy
55import os
66import re
7+ import threading
78import weakref
89import logging
910
@@ -164,8 +165,17 @@ def _getValue(self):
164165 raise RuntimeError (f"Cannot get value of { self ._getFullName ()} , the attribute is keyable." )
165166 if self .isLink :
166167 return self ._getInputLink ().value
168+ self ._resolveValue ()
167169 return self ._value
168170
171+ def _resolveValue (self ):
172+ """
173+ Hook for subclasses to resolve pending values before returning _value.
174+ Called by _getValue before returning self._value.
175+ Default implementation is a no-op.
176+ """
177+ pass
178+
169179 def _setValue (self , value ):
170180 """
171181 Set the attribute value from a given value, a given function or a given attribute.
@@ -775,6 +785,9 @@ def getSerializedValue(self):
775785
776786class ListAttribute (Attribute ):
777787
788+ # Sentinel to distinguish 'no pending dynamic value' from 'pending reset to empty'
789+ _NO_PENDING_VALUE = object ()
790+
778791 def __init__ (self , node , attributeDesc : desc .ListAttribute , isOutput : bool ,
779792 root = None , parent = None ):
780793 super ().__init__ (node , attributeDesc , isOutput , root , parent )
@@ -816,13 +829,14 @@ def insert(self, index, value):
816829 self ._value .insert (index , attrs )
817830 self .valueChanged .emit ()
818831 self ._applyExpr ()
819- self .requestGraphUpdate ()
832+ if self .isInput :
833+ self .requestGraphUpdate ()
820834
821835 @raiseIfLink
822836 def remove (self , index , count = 1 ):
823837 if self ._value is None :
824838 return
825- if self .node .graph :
839+ if self .node .graph and self . isInput :
826840 from meshroom .core .graph import GraphModification
827841 with GraphModification (self .node .graph ):
828842 # remove potential links
@@ -832,15 +846,41 @@ def remove(self, index, count=1):
832846 # delete edge if the attribute is linked
833847 self .node .graph .removeEdge (attr )
834848 self ._value .removeAt (index , count )
835- self .requestGraphUpdate ()
849+ if self .isInput :
850+ self .requestGraphUpdate ()
836851 self .valueChanged .emit ()
837852
838853 # Override
839854 def _initValue (self ):
855+ self ._dynamicValueLock = threading .Lock ()
856+ self ._dynamicValue = ListAttribute ._NO_PENDING_VALUE
840857 self .resetToDefaultValue ()
841858
842859 # Override
843860 def _setValue (self , value ):
861+ if self .isOutput :
862+ # For output attributes (set during processChunk in a worker thread,
863+ # or during loadOutputAttr in the TaskThread), store raw values without
864+ # creating QObject children to avoid cross-thread parenting issues.
865+ # The raw values are:
866+ # - serialized by saveOutputAttr via getPrimitiveValue
867+ # - materialized into QObjects lazily by _getValue on the main thread
868+ with self ._dynamicValueLock :
869+ if value is None :
870+ self ._dynamicValue = None # pending reset
871+ else :
872+ self ._dynamicValue = self ._desc .validateValue (value )
873+ return
874+
875+ # Input attribute path: handle None
876+ if value is None :
877+ if self .node .graph and self ._value is not None and len (self ._value ) > 0 :
878+ self .remove (0 , len (self ))
879+ if self ._value is None :
880+ self ._value = ListModel (parent = self )
881+ self .valueChanged .emit ()
882+ return
883+
844884 if self .node .graph :
845885 self .remove (0 , len (self ))
846886 if self ._handleLinkValue (value ):
@@ -852,7 +892,8 @@ def _setValue(self, value):
852892 self ._value = ListModel (parent = self )
853893 newValue = self ._desc .validateValue (value )
854894 self .extend (newValue )
855- self .requestGraphUpdate ()
895+ if self .isInput :
896+ self .requestGraphUpdate ()
856897
857898 # Override
858899 def _applyExpr (self ):
@@ -862,6 +903,20 @@ def _applyExpr(self):
862903 for value in self ._value :
863904 value ._applyExpr ()
864905
906+ def _populateFromDynamicValue (self , value ):
907+ """Store raw dynamic values for lazy materialization.
908+
909+ Does NOT create QObject children — safe to call from any thread.
910+ The actual ListModel population happens lazily in _getValue()
911+ when the main thread (e.g. QML) reads the value.
912+ """
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+ self .valueChanged .emit ()
919+
865920 # Override
866921 def resetToDefaultValue (self ):
867922 self ._value = ListModel (parent = self )
@@ -877,8 +932,57 @@ def getSerializedValue(self):
877932 return self ._getInputLink ().asLinkExpr ()
878933 return [attr .getSerializedValue () for attr in self ._value ]
879934
935+ value = Property (Variant , Attribute ._getValue , _setValue , notify = Attribute .valueChanged )
936+
937+ # Override
938+ def _resolveValue (self ):
939+ """
940+ Lazily materialize QObject children from pending raw dynamic values.
941+ Called by Attribute._getValue (base) before returning self._value.
942+ This hook dispatches via normal Python MRO, bypassing PySide Property
943+ getter dispatch limitations.
944+ Must only create QObjects on the main thread to avoid cross-thread issues.
945+ """
946+ if self ._dynamicValue is not ListAttribute ._NO_PENDING_VALUE :
947+ if threading .current_thread () is threading .main_thread ():
948+ self ._materializeDynamicValue ()
949+
950+ def _materializeDynamicValue (self ):
951+ """
952+ Create QObject children in the ListModel from pending raw dynamic values.
953+ Must only be called on the main thread.
954+ """
955+
956+ # Thread proof reading of dynamic value
957+ with self ._dynamicValueLock :
958+ pendingValue = self ._dynamicValue
959+ self ._dynamicValue = ListAttribute ._NO_PENDING_VALUE
960+
961+ # Create an empty list if the value is None
962+ if self ._value is None :
963+ self ._value = ListModel (parent = self )
964+ elif len (self ._value ) > 0 :
965+ # Erase all items before reassigning
966+ self ._value .removeAt (0 , len (self ._value ))
967+
968+ # Effectively create the objects from the raw data
969+ if pendingValue :
970+ attrs = [attributeFactory (self ._desc .elementDesc , v , self .isOutput , self .node , self )
971+ for v in pendingValue ]
972+ self ._value .insert (0 , attrs )
973+
974+ self .valueChanged .emit ()
975+
880976 # Override
881977 def getPrimitiveValue (self , exportDefault = True ):
978+ # If there is a pending dynamic value (set or reset), return it directly
979+ # without touching the ListModel (which may be on a different thread).
980+ with self ._dynamicValueLock :
981+ if self ._dynamicValue is not ListAttribute ._NO_PENDING_VALUE :
982+ if self ._dynamicValue is None :
983+ return []
984+ return list (self ._dynamicValue )
985+
882986 if exportDefault :
883987 return [attr .getPrimitiveValue (exportDefault = exportDefault ) for attr in self ._value ]
884988 return [attr .getPrimitiveValue (exportDefault = exportDefault ) for attr in self ._value
0 commit comments