diff --git a/Changes.md b/Changes.md
index 284e756d453..77921afb0fc 100644
--- a/Changes.md
+++ b/Changes.md
@@ -233,6 +233,7 @@ API
- Added support for creating a `PathMatcherDataPlug` from `PathMatcherData` in `createPlugFromData()`.
- Added support for getting `PathMatcherData` from a `PathMatcherDataPlug` in `getValueAsData()`.
- EditScopeAlgo : Added support for editing options.
+- BackgroundTask : A warning is now emitted if the `subject` can not be associated with a ScriptNode for cancellation purposes.
1.2.8.0 (relative to 1.2.7.0)
=======
diff --git a/python/GafferImageTest/MedianTest.py b/python/GafferImageTest/MedianTest.py
index 9ebaf1c64ff..d3ccc462be9 100644
--- a/python/GafferImageTest/MedianTest.py
+++ b/python/GafferImageTest/MedianTest.py
@@ -145,13 +145,14 @@ def testDriverChannel( self ) :
def testCancellation( self ) :
- c = GafferImage.Constant()
+ script = Gaffer.ScriptNode()
+ script["c"] = GafferImage.Constant()
- m = GafferImage.Median()
- m["in"].setInput( c["out"] )
- m["radius"].setValue( imath.V2i( 2000 ) )
+ script["m"] = GafferImage.Median()
+ script["m"]["in"].setInput( script["c"]["out"] )
+ script["m"]["radius"].setValue( imath.V2i( 2000 ) )
- bt = Gaffer.ParallelAlgo.callOnBackgroundThread( m["out"], lambda : GafferImageTest.processTiles( m["out"] ) )
+ bt = Gaffer.ParallelAlgo.callOnBackgroundThread( script["m"]["out"], lambda : GafferImageTest.processTiles( script["m"]["out"] ) )
# Give background tasks time to get into full swing
time.sleep( 0.1 )
@@ -163,9 +164,9 @@ def testCancellation( self ) :
# Check that we can do the same when using a master
# channel.
- m["masterChannel"].setValue( "R" )
+ script["m"]["masterChannel"].setValue( "R" )
- bt = Gaffer.ParallelAlgo.callOnBackgroundThread( m["out"], lambda : GafferImageTest.processTiles( m["out"] ) )
+ bt = Gaffer.ParallelAlgo.callOnBackgroundThread( script["m"]["out"], lambda : GafferImageTest.processTiles( script["m"]["out"] ) )
time.sleep( 0.1 )
t = time.time()
diff --git a/python/GafferImageTest/ResampleTest.py b/python/GafferImageTest/ResampleTest.py
index 0f8f3ce39f0..0acd3fe43cc 100644
--- a/python/GafferImageTest/ResampleTest.py
+++ b/python/GafferImageTest/ResampleTest.py
@@ -188,13 +188,15 @@ def testExpandDataWindow( self ) :
def testCancellation( self ) :
- c = GafferImage.Constant()
+ script = Gaffer.ScriptNode()
- r = GafferImage.Resample()
- r["in"].setInput( c["out"] )
- r["filterScale"].setValue( imath.V2f( 2000 ) )
+ script["c"] = GafferImage.Constant()
+
+ script["r"] = GafferImage.Resample()
+ script["r"]["in"].setInput( script["c"]["out"] )
+ script["r"]["filterScale"].setValue( imath.V2f( 2000 ) )
- bt = Gaffer.ParallelAlgo.callOnBackgroundThread( r["out"], lambda : GafferImageTest.processTiles( r["out"] ) )
+ bt = Gaffer.ParallelAlgo.callOnBackgroundThread( script["r"]["out"], lambda : GafferImageTest.processTiles( script["r"]["out"] ) )
# Give background tasks time to get into full swing
time.sleep( 0.1 )
@@ -205,9 +207,9 @@ def testCancellation( self ) :
self.assertLess( time.time() - t, acceptableCancellationDelay )
# Check that we can do the same when using a non-separable filter
- r["filter"].setValue( "disk" )
+ script["r"]["filter"].setValue( "disk" )
- bt = Gaffer.ParallelAlgo.callOnBackgroundThread( r["out"], lambda : GafferImageTest.processTiles( r["out"] ) )
+ bt = Gaffer.ParallelAlgo.callOnBackgroundThread( script["r"]["out"], lambda : GafferImageTest.processTiles( script["r"]["out"] ) )
time.sleep( 0.1 )
t = time.time()
diff --git a/python/GafferSceneTest/RenderControllerTest.py b/python/GafferSceneTest/RenderControllerTest.py
index 235d0cb5669..e6f3b4b37e8 100644
--- a/python/GafferSceneTest/RenderControllerTest.py
+++ b/python/GafferSceneTest/RenderControllerTest.py
@@ -844,21 +844,23 @@ def testIDs( self ) :
def testProgressCallback( self ) :
- sphere = GafferScene.Sphere()
- group = GafferScene.Group()
- group["in"][0].setInput( sphere["out"] )
- group["in"][1].setInput( sphere["out"] )
- group["in"][2].setInput( sphere["out"] )
+ script = Gaffer.ScriptNode()
- allFilter = GafferScene.PathFilter()
- allFilter["paths"].setValue( IECore.StringVectorData( [ "/*", "/*/*" ] ) )
+ script["sphere"] = GafferScene.Sphere()
+ script["group"] = GafferScene.Group()
+ script["group"]["in"][0].setInput( script["sphere"]["out"] )
+ script["group"]["in"][1].setInput( script["sphere"]["out"] )
+ script["group"]["in"][2].setInput( script["sphere"]["out"] )
+
+ script["allFilter"] = GafferScene.PathFilter()
+ script["allFilter"]["paths"].setValue( IECore.StringVectorData( [ "/*", "/*/*" ] ) )
- transform = GafferScene.Transform()
- transform["in"].setInput( group["out"] )
- transform["filter"].setInput( allFilter["out"] )
+ script["transform"] = GafferScene.Transform()
+ script["transform"]["in"].setInput( script["group"]["out"] )
+ script["transform"]["filter"].setInput( script["allFilter"]["out"] )
renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer()
- controller = GafferScene.RenderController( transform["out"], Gaffer.Context(), renderer )
+ controller = GafferScene.RenderController( script["transform"]["out"], Gaffer.Context(), renderer )
controller.setMinimumExpansionDepth( 2 )
statuses = []
@@ -882,7 +884,7 @@ def callback( status ) :
# path we requested, and it's ancestors (not including the root, because
# its transform hasn't changed).
del statuses[:]
- transform["transform"]["translate"]["x"].setValue( 1 )
+ script["transform"]["transform"]["translate"]["x"].setValue( 1 )
controller.updateMatchingPaths( IECore.PathMatcher( [ "/group/sphere" ] ), callback )
self.assertEqual( statuses, [ Status.Running ] * 2 + [ Status.Completed ] )
@@ -890,7 +892,7 @@ def callback( status ) :
# We expect updates to all locations (except the root, because its transform
# hasn't changed).
del statuses[:]
- transform["transform"]["translate"]["x"].setValue( 2 )
+ script["transform"]["transform"]["translate"]["x"].setValue( 2 )
task = controller.updateInBackground( callback, priorityPaths = IECore.PathMatcher( [ "/group/sphere" ] ) )
task.wait()
self.assertEqual( statuses, [ Status.Running ] * 4 + [ Status.Completed ] )
diff --git a/python/GafferSceneUITest/CropWindowToolTest.py b/python/GafferSceneUITest/CropWindowToolTest.py
index 43d6f879c51..cd9f1408033 100644
--- a/python/GafferSceneUITest/CropWindowToolTest.py
+++ b/python/GafferSceneUITest/CropWindowToolTest.py
@@ -46,9 +46,11 @@ class CropWindowToolTest( GafferUITest.TestCase ) :
def testSceneViewStatus( self ) :
- camera = GafferScene.Camera()
+ script = Gaffer.ScriptNode()
+
+ script["camera"] = GafferScene.Camera()
- view = GafferUI.View.create( camera["out"] )
+ view = GafferUI.View.create( script["camera"]["out"] )
tool = GafferSceneUI.CropWindowTool( view )
tool["active"].setValue( True )
@@ -75,28 +77,28 @@ def testSceneViewStatus( self ) :
# Editable
- options = GafferScene.StandardOptions()
- options["in"].setInput( camera["out"] )
- view["in"].setInput( options["out"] )
+ script["options"] = GafferScene.StandardOptions()
+ script["options"]["in"].setInput( script["camera"]["out"] )
+ view["in"].setInput( script["options"]["out"] )
self.waitForIdle( 1000 )
- self.assertEqual( tool.status(), "Info: Editing StandardOptions.options.renderCropWindow.value" )
+ self.assertEqual( tool.status(), "Info: Editing options.options.renderCropWindow.value" )
# Locked value plug
- Gaffer.MetadataAlgo.setReadOnly( options["options"]["renderCropWindow"]["value"], True )
+ Gaffer.MetadataAlgo.setReadOnly( script["options"]["options"]["renderCropWindow"]["value"], True )
self.waitForIdle( 1000 )
- self.assertEqual( tool.status(), "Warning: StandardOptions.options.renderCropWindow.value is locked" )
+ self.assertEqual( tool.status(), "Warning: options.options.renderCropWindow.value is locked" )
# Locked/off enabled plug
- Gaffer.MetadataAlgo.setReadOnly( options["options"]["renderCropWindow"]["value"], False )
- options["options"]["renderCropWindow"]["enabled"].setValue( False )
- Gaffer.MetadataAlgo.setReadOnly( options["options"]["renderCropWindow"]["enabled"], True )
+ Gaffer.MetadataAlgo.setReadOnly( script["options"]["options"]["renderCropWindow"]["value"], False )
+ script["options"]["options"]["renderCropWindow"]["enabled"].setValue( False )
+ Gaffer.MetadataAlgo.setReadOnly( script["options"]["options"]["renderCropWindow"]["enabled"], True )
self.waitForIdle( 1000 )
- self.assertEqual( tool.status(), "Warning: StandardOptions.options.renderCropWindow.value isn't editable" )
+ self.assertEqual( tool.status(), "Warning: options.options.renderCropWindow.value isn't editable" )
# Check status across visible/invisible overlay transitions (this is
# really testing one of the gnarly parts of the status implementation
@@ -110,7 +112,7 @@ def testSceneViewStatus( self ) :
view["camera"]["lookThroughEnabled"].setValue( True )
self.waitForIdle( 1000 )
- self.assertEqual( tool.status(), "Warning: StandardOptions.options.renderCropWindow.value isn't editable" )
+ self.assertEqual( tool.status(), "Warning: options.options.renderCropWindow.value isn't editable" )
def testImageViewStatus( self ) :
diff --git a/python/GafferSceneUITest/SceneGadgetTest.py b/python/GafferSceneUITest/SceneGadgetTest.py
index 9b0eefd5548..efa2a34f842 100644
--- a/python/GafferSceneUITest/SceneGadgetTest.py
+++ b/python/GafferSceneUITest/SceneGadgetTest.py
@@ -371,23 +371,25 @@ def testExceptionsDuringCompute( self ) :
def testObjectsAtBox( self ) :
- plane = GafferScene.Plane()
+ script = Gaffer.ScriptNode()
- sphere = GafferScene.Sphere()
- sphere["radius"].setValue( 0.25 )
+ script["plane"] = GafferScene.Plane()
- instancer = GafferScene.Instancer()
- instancer["in"].setInput( plane["out"] )
- instancer["prototypes"].setInput( sphere["out"] )
- instancer["parent"].setValue( "/plane" )
+ script["sphere"] = GafferScene.Sphere()
+ script["sphere"]["radius"].setValue( 0.25 )
- subTree = GafferScene.SubTree()
- subTree["in"].setInput( instancer["out"] )
- subTree["root"].setValue( "/plane" )
+ script["instancer"] = GafferScene.Instancer()
+ script["instancer"]["in"].setInput( script["plane"]["out"] )
+ script["instancer"]["prototypes"].setInput( script["sphere"]["out"] )
+ script["instancer"]["parent"].setValue( "/plane" )
+
+ script["subTree"] = GafferScene.SubTree()
+ script["subTree"]["in"].setInput( script["instancer"]["out"] )
+ script["subTree"]["root"].setValue( "/plane" )
sg = GafferSceneUI.SceneGadget()
sg.setRenderer( self.renderer )
- sg.setScene( subTree["out"] )
+ sg.setScene( script["subTree"]["out"] )
sg.setMinimumExpansionDepth( 100 )
with GafferUI.Window() as w :
@@ -436,21 +438,19 @@ def testObjectsAtBox( self ) :
def testObjectAtLine( self ) :
- cubes = []
- names = ( "left", "center", "right" )
- for i in range( 3 ) :
+ script = Gaffer.ScriptNode()
+ script["group"] = GafferScene.Group()
+
+ for i, name in enumerate( [ "left", "center", "right" ] ) :
cube = GafferScene.Cube()
cube["transform"]["translate"].setValue( imath.V3f( ( i - 1 ) * 2.0, 0.0, -2.5 ) )
- cube["name"].setValue( names[i] )
- cubes.append( cube )
-
- group = GafferScene.Group()
- for i, cube in enumerate( cubes ) :
- group["in"][i].setInput( cube["out"] )
+ cube["name"].setValue( name )
+ script.addChild( cube )
+ script["group"]["in"][i].setInput( cube["out"] )
sg = GafferSceneUI.SceneGadget()
sg.setRenderer( self.renderer )
- sg.setScene( group["out"] )
+ sg.setScene( script["group"]["out"] )
sg.setMinimumExpansionDepth( 100 )
with GafferUI.Window() as w :
@@ -532,13 +532,15 @@ def testSetAndGetScene( self ) :
def testBoundOfUnexpandedEmptyChildren( self ) :
- group1 = GafferScene.Group()
- group2 = GafferScene.Group()
- group2["in"][0].setInput( group1["out"] )
+ script = Gaffer.ScriptNode()
+
+ script["group1"] = GafferScene.Group()
+ script["group2"] = GafferScene.Group()
+ script["group2"]["in"][0].setInput( script["group1"]["out"] )
sg = GafferSceneUI.SceneGadget()
sg.setRenderer( self.renderer )
- sg.setScene( group2["out"] )
+ sg.setScene( script["group2"]["out"] )
self.waitForRender( sg )
self.assertEqual( sg.bound(), imath.Box3f() )
@@ -563,18 +565,20 @@ def testSelectionMaskAccessors( self ) :
def testSelectionMask( self ) :
- plane = GafferScene.Plane()
- plane["dimensions"].setValue( imath.V2f( 10 ) )
- plane["transform"]["translate"]["z"].setValue( 4 )
+ script = Gaffer.ScriptNode()
+
+ script["plane"] = GafferScene.Plane()
+ script["plane"]["dimensions"].setValue( imath.V2f( 10 ) )
+ script["plane"]["transform"]["translate"]["z"].setValue( 4 )
- camera = GafferScene.Camera()
- group = GafferScene.Group()
- group["in"][0].setInput( plane["out"] )
- group["in"][1].setInput( camera["out"] )
+ script["camera"] = GafferScene.Camera()
+ script["group"] = GafferScene.Group()
+ script["group"]["in"][0].setInput( script["plane"]["out"] )
+ script["group"]["in"][1].setInput( script["camera"]["out"] )
sg = GafferSceneUI.SceneGadget()
sg.setRenderer( self.renderer )
- sg.setScene( group["out"] )
+ sg.setScene( script["group"]["out"] )
sg.setMinimumExpansionDepth( 100 )
with GafferUI.Window() as w :
diff --git a/python/GafferTest/ValuePlugTest.py b/python/GafferTest/ValuePlugTest.py
index cb0bb0d7525..abe401b6bc2 100644
--- a/python/GafferTest/ValuePlugTest.py
+++ b/python/GafferTest/ValuePlugTest.py
@@ -953,13 +953,14 @@ def computeCachePolicy( self, output ) :
# in `LRUCachePolicy.TaskParallel.Handle.acquire`.
) :
- node = InfiniteLoop( cachePolicy = cachePolicy )
+ script = Gaffer.ScriptNode()
+ script["node"] = InfiniteLoop( cachePolicy = cachePolicy )
# Launch a compute in the background, and wait for it to start.
- with node.computeStartedCondition :
- backgroundTask1 = Gaffer.ParallelAlgo.callOnBackgroundThread( node["out"], lambda : node["out"].getValue() )
- node.computeStartedCondition.wait()
+ with script["node"].computeStartedCondition :
+ backgroundTask1 = Gaffer.ParallelAlgo.callOnBackgroundThread( script["node"]["out"], lambda : script["node"]["out"].getValue() )
+ script["node"].computeStartedCondition.wait()
# Launch a second compute in the background, wait for it to start, and
# then make sure we can cancel it even though the compute is already in
@@ -973,10 +974,10 @@ def getValueExpectingCancellation() :
startedCondition.notify()
with self.assertRaises( IECore.Cancelled ) :
- node["out"].getValue()
+ script["node"]["out"].getValue()
with startedCondition :
- backgroundTask2 = Gaffer.ParallelAlgo.callOnBackgroundThread( node["out"], getValueExpectingCancellation )
+ backgroundTask2 = Gaffer.ParallelAlgo.callOnBackgroundThread( script["node"]["out"], getValueExpectingCancellation )
startedCondition.wait()
backgroundTask2.cancelAndWait()
diff --git a/python/GafferUITest/BoolPlugValueWidgetTest.py b/python/GafferUITest/BoolPlugValueWidgetTest.py
index abd4b5758a6..6c9651cc6e8 100644
--- a/python/GafferUITest/BoolPlugValueWidgetTest.py
+++ b/python/GafferUITest/BoolPlugValueWidgetTest.py
@@ -83,18 +83,20 @@ def testInitialValue( self ) :
def testErrorHandling( self ) :
- n = Gaffer.Node()
- n["user"]["p"] = Gaffer.BoolPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic )
+ script = Gaffer.ScriptNode()
+
+ script["n"] = Gaffer.Node()
+ script["n"]["user"]["p"] = Gaffer.BoolPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic )
- w = GafferUI.BoolPlugValueWidget( n["user"]["p"] )
+ w = GafferUI.BoolPlugValueWidget( script["n"]["user"]["p"] )
self.assertFalse( w.boolWidget().getErrored() )
- b = GafferTest.BadNode()
- n["user"]["p"].setInput( b["out3"] )
+ script["b"] = GafferTest.BadNode()
+ script["n"]["user"]["p"].setInput( script["b"]["out3"] )
GafferUITest.PlugValueWidgetTest.waitForUpdate( w )
self.assertTrue( w.boolWidget().getErrored() )
- n["user"]["p"].setInput( None )
+ script["n"]["user"]["p"].setInput( None )
GafferUITest.PlugValueWidgetTest.waitForUpdate( w )
self.assertFalse( w.boolWidget().getErrored() )
diff --git a/python/GafferUITest/StringPlugValueWidgetTest.py b/python/GafferUITest/StringPlugValueWidgetTest.py
index 099bcbfdf70..27bdba366ed 100644
--- a/python/GafferUITest/StringPlugValueWidgetTest.py
+++ b/python/GafferUITest/StringPlugValueWidgetTest.py
@@ -141,15 +141,17 @@ def testMixedValuesPreserved( self ) :
def testExceptionHandling( self ) :
- # Compute for `n["p"]` will error because it's an output plug
+ # Compute for `script["n"]["p"]` will error because it's an output plug
# that ComputeNode knows nothing about.
- n = Gaffer.ComputeNode()
- n["p"] = Gaffer.StringPlug( direction = Gaffer.Plug.Direction.Out )
+ script = Gaffer.ScriptNode()
+
+ script["n"] = Gaffer.ComputeNode()
+ script["n"]["p"] = Gaffer.StringPlug( direction = Gaffer.Plug.Direction.Out )
# We want that to be reflected in the UI.
- w = GafferUI.StringPlugValueWidget( n["p"] )
+ w = GafferUI.StringPlugValueWidget( script["n"]["p"] )
GafferUITest.PlugValueWidgetTest.waitForUpdate( w )
self.assertEqual( w.textWidget().getText(), "" )
self.assertTrue( w.textWidget().getErrored() )
diff --git a/python/GafferVDBTest/MeshToLevelSetTest.py b/python/GafferVDBTest/MeshToLevelSetTest.py
index e566d6fb93f..509b53c081f 100644
--- a/python/GafferVDBTest/MeshToLevelSetTest.py
+++ b/python/GafferVDBTest/MeshToLevelSetTest.py
@@ -135,19 +135,21 @@ def testCancellation( self ) :
# Start a computation in the background, and
# then cancel it.
- sphere = GafferScene.Sphere()
+ script = Gaffer.ScriptNode()
- meshToLevelSet = GafferVDB.MeshToLevelSet()
- meshToLevelSet["in"].setInput( sphere["out"] )
- self.setFilter( meshToLevelSet, path = "/sphere" )
- meshToLevelSet["voxelSize"].setValue( 0.01 )
+ script["sphere"] = GafferScene.Sphere()
+
+ script["meshToLevelSet"] = GafferVDB.MeshToLevelSet()
+ script["meshToLevelSet"]["in"].setInput( script["sphere"]["out"] )
+ self.setFilter( script["meshToLevelSet"], path = "/sphere" )
+ script["meshToLevelSet"]["voxelSize"].setValue( 0.01 )
def computeObject() :
- meshToLevelSet["out"].object( "/sphere" )
+ script["meshToLevelSet"]["out"].object( "/sphere" )
backgroundTask = Gaffer.ParallelAlgo.callOnBackgroundThread(
- meshToLevelSet["out"]["object"], computeObject
+ script["meshToLevelSet"]["out"]["object"], computeObject
)
# Delay so that the computation actually starts, rather
# than being avoided entirely.
@@ -157,11 +159,11 @@ def computeObject() :
# Get the value again. If cancellation has been managed properly, this
# will do a fresh compute to get a full result, and not pull a half-finished
# result out of the cache.
- vdbAfterCancellation = meshToLevelSet["out"].object( "/sphere" )
+ vdbAfterCancellation = script["meshToLevelSet"]["out"].object( "/sphere" )
# Compare against a result computed from scratch.
Gaffer.ValuePlug.clearCache()
- vdb = meshToLevelSet["out"].object( "/sphere" )
+ vdb = script["meshToLevelSet"]["out"].object( "/sphere" )
self.assertEqual(
vdbAfterCancellation.findGrid( "surface" ).activeVoxelCount(),
diff --git a/src/Gaffer/BackgroundTask.cpp b/src/Gaffer/BackgroundTask.cpp
index 8721e8f1015..3ae2c87ca22 100644
--- a/src/Gaffer/BackgroundTask.cpp
+++ b/src/Gaffer/BackgroundTask.cpp
@@ -48,6 +48,8 @@
#include "tbb/task_arena.h"
+#include "fmt/format.h"
+
using namespace IECore;
using namespace Gaffer;
@@ -151,7 +153,13 @@ struct BackgroundTask::TaskData : public boost::noncopyable
BackgroundTask::BackgroundTask( const Plug *subject, const Function &function )
: m_function( function ), m_taskData( std::make_shared( &m_function ) )
{
- activeTasks().insert( ActiveTask{ this, scriptNode( subject ) } );
+ const ScriptNode *s = scriptNode( subject );
+ if( subject && !s )
+ {
+ IECore::msg( IECore::Msg::Level::Warning, "BackgroundTask", fmt::format( "Unable to find ScriptNode for {}", subject->fullName() ) );
+ }
+
+ activeTasks().insert( ActiveTask{ this, s } );
// Enqueue task into current arena.
tbb::task_arena( tbb::task_arena::attach() ).enqueue(