Implementing Tile rendering into custom pipeline

After successfully implementing DBR into our toolset, I’d like to include tile rendering as an option. I don’t need the fancy jig-saw rendering for automated batch-rendering, but being able to say we want our image to be split into 10x10 tiles is a useful option that doesn’t require setting up regions for every camera/render.

So far, I was able to send a job that renders the tiles, but I’ve had no luck submitting the DRAFT assembly job. I’m using the following MAXscript statements (taken from analysing parts of the SMTD scripts):

SMTDSettings.RegionRenderingMode = #singleFrameTiles SMTDSettings.SingleTileJobDraft = true SMTDSettings.SingleTileJobCleanup = true SMTDSettings.SingleTileJobDependent = true SMTDSettings.TilesInY = 10 SMTDSettings.TilesInX = 10

The renderjob is submitted, the tile info has been correctly added to the submission parameters. The DRAFT assembly job, however, has not been created. I haven’t found where this job is made, and I haven’t found anything in the documentation that would help me figure out what I’m missing to implement this in our pipeline.

I presume the code to generate a DRAFT script for this and submit it to deadline has already been written, I just haven’t found it yet…

Unfortunately, it is difficult to answer your question without knowing which other functions you are calling to perform the submission.
The function that creates the Draft assembly job is called, obviously, SMTDFunctions.SubmitDraftTileAssemblerJob(). It is being called from inside the SMTDFunctions.spawnTileJobs(). That one is being called from inside SMTDFunctions.SubmitJobFromUI(), which is what gets called when the user presses the SUBMIT button in the SMTD UI. But if you are not using SubmitJobFromUI() and have your own code that creates all the files and calls the submitter with your own arguments, then the rest of the chain won’t be called.

Also, some Tile modes do NOT support Draft Assembly. Thankfully, the Single Frame Tile Rendering does.
But the arguments list of the SMTDFunctions.SubmitDraftTileAssemblerJob() is quite complex, so if you are NOT calling SMTDFunctions.spawnTileJobs(), then you will have to read through the SMTDFunctions.spawnTileJobs() body and see what data is being collected, and provide it yourself.

Can you send me your current script so I can see what you are doing?
Or would you prefer if I would take the simple submission script tutorial code from our documentation pages and show how to expand it to do Tile Rendering and Draft Assembly?

I went ahead and modified the UI version of the simple scripted submission tutorial deadline.thinkboxsoftware.com/cu … submission

Note that the single call to SMTDFunctions.spawnTileJobs() handles both the Max scene submission, and the Draft Tile Assembler submission for any number of output files (incl. all Render Elements).

Looks for the --NEW CODE STARTS HERE remark:

(
global SMTDSettings
global SMTDFunctions

local theResultFile = getDir #temp + "\\_result.txt"
local theCommandLine = ("deadlinecommand.exe -getrepositoryroot > \""+theResultFile +"\"")
hiddenDosCommand theCommandLine startpath:"c:\\"
local theFileHandle = openFile theResultFile
local theNetworkRoot= readLine theFileHandle
close theFileHandle

global SimpleDeadlineSubmitter_Rollout
try(destroyDialog SimpleDeadlineSubmitter_Rollout)catch()

rollout SimpleDeadlineSubmitter_Rollout "Simple Deadline Submitter"
(
spinner spn_priority "Priority:" range:[1,100,SMTDSettings.Priority] type:#integer fieldwidth:50
spinner spn_chunkSize "Frames Per Task:" range:[1,100,SMTDSettings.ChunkSize] type:#integer fieldwidth:50
checkbox chk_limitEnabled "" across:2 checked:SMTDSettings.LimitEnabled
spinner spn_machineLimit "Machine Limit:" range:[1,100,SMTDSettings.MachineLimit] type:#integer fieldwidth:50
button btn_submit "SUBMIT SCENE..."width:190 height:30 align:#center
on btn_submit pressed do
(

local remoteScript = theNetworkRoot + @"\submission\3dsmax\main\SubmitMaxToDeadline_Functions.ms" 
local localScript = getDir #userscripts + "\\SubmitMaxToDeadline_Functions.ms"
if doesFileExist remoteScript do
(
if SMTDFunctions == undefined do
(
deleteFile localScript
copyFile remoteScript localScript
fileIn localScript
)

SMTDFunctions.loadSettings()
SMTDSettings.JobName = maxFileName + " [SIMPLE MXS TILE SUBMISSION]"
SMTDSettings.Comment = "Testing Tile Submission And Draft Assembly."
SMTDSettings.Priority = spn_priority.value
SMTDSettings.ChunkSize = spn_chunkSize.value
SMTDSettings.LimitEnabled = chk_limitEnabled.checked
SMTDSettings.MachineLimit = spn_machineLimit.value

local maxFileToSubmit = SMTDPaths.tempdir + maxFileName
SMTDFunctions.SaveMaxFileCopy maxFileToSubmit

-- NEW CODE STARTS HERE

-- Set up Single Frame Tile Rendering
SMTDSettings.RegionRenderingMode = #singleFrameTiles
SMTDSettings.SingleTileJobDraft = true
SMTDSettings.SingleTileJobCleanup = true
SMTDSettings.SingleTileJobDependent = true
SMTDSettings.TilesInY = 10
SMTDSettings.TilesInX = 10

-- Call the function that submits the Max scene and the Assembly Job as dependent:
local tileResult = SMTDFunctions.spawnTileJobs forceMaxFile:maxFileToSubmit batchName:(maxFileName+ " Tiles") 

-- The function returns true on success, false on failure, so we translate that into the result name value 
if tileResult then result = #success else result = #tileFailure
	
-- NEW CODE ENDS HERE

local renderMsg = SMTDFunctions.getRenderMessage()
SMTDFunctions.getJobIDFromMessage renderMsg

if result == #success then
(
format "Submitted successfully as Job %.\n\n%\n\n" \
SMTDSettings.DeadlineSubmissionLastJobID renderMsg
)
else
format "Job Submission FAILED.\n\n%" renderMsg
)--end if
)--end on button pressed
)--end rollout
createDialog SimpleDeadlineSubmitter_Rollout width:200
)--end script

First of all, I didn’t expect to get help from the great Bobo himself! When I was first starting out with maxscript, your posts on forums were invaluable, so thanks for that!

As for the code, I’ve created a set of global functions that I can call in all of my scripts (or even the listener, should I want to) that enable the different tools to submit the current scene to deadline. Obviously, I shouldn’t be changing the signature of the function, so I don’t have to open every single tool I’ve made to update it. But I don’t think this will be needed.

I think I will have to go through all the code though, because I’m using batching functionality. I create a queue that holds the maxfile, plugin and jobinfofiles and a boolean to make it dependent on the previous job. I have a SubmitToQueue function that will add this information to an array, and then ultimately I call a FlushQueue function that will output that data to a string and then I use the DeadlineCommandbg.exe -multi flag to submit all those jobs at once. This seemed the most efficient way, as oftentimes we send out large batches of renders and it’s faster this way. Even the simplest of chains (Lightcache pass, irradiance pass, beauty pass, each depending on the previous) benefits from this setup as I don’t have to submit every job by itself just to get the ID to use for dependencies.

I will post the code here anyways, in case I can’t figure it out by the time you see this :slight_smile:

[code]global DeadlineJob
struct DeadlineJob
(
sJobFile,
sPluginFile,
sMaxfile,
bDependsOnPrevious = false
)
global aDeadlineQueue = #()
global sDeadlineQueuePath = undefined
global InitDeadlineQueue
global SMTDSettings
global SMTDFunctions
fn InitDeadlineQueue sDescription:unsupplied =
(
theResultFile = getDir #temp + “\_result.txt”
theCommandLine = (“deadlinecommand.exe -getrepositoryroot > “”+theResultFile +”"")
hiddenDosCommand theCommandLine startpath:“C:\Program Files\Thinkbox\Deadline7\bin\”
theFileHandle = openFile theResultFile
theNetworkRoot = readLine theFileHandle
close theFileHandle

remoteScript = theNetworkRoot + "\\submission\\3dsmax\\main\\SubmitMaxToDeadline_Functions.ms"  
localScript = getDir #userscripts + "\\SubmitMaxToDeadline_Functions.ms"
if doesFileExist remoteScript do
(
	if SMTDFunctions == undefined do
	(
		deleteFile localScript
		copyFile remoteScript localScript
		fileIn localScript
	)
)

aDate = getlocaltime()
sDate = aDate[1] as string + FormatNumber 2 aDate[2] + FormatNumber 2 aDate[4] + "_" + FormatNumber 2 aDate[5] + FormatNumber 2 aDate[6]
sDeadlineQueuePath = (getdir #temp) + "DeadlineJobInfoFiles\\" + sDate
if sDescription != unsupplied then
(
	sDeadlineQueuePath += "_" + sDescription
)
sDeadlineQueuePath += "\\"
makedir sDeadlineQueuePath all:true

aDeadlineQueue = #()

)
– InitDeadlineQueue()

global FlushDeadlineQueue
fn FlushDeadlineQueue bFeedback:false =
(
sCommandFile = (getdir #temp + “\” + sysinfo.username + “.txt”)
oFlags = openfile sCommandFile mode:“w”
sCommand = “DeadlineCommandbg.exe “” + sCommandFile + “””

format " -multi\n" to:oFlags
if bFeedback then
(
	format " -notify\n" to:oFlags
)

for job in aDeadlineQueue do
(
	format " -job\n" to:oFlags
	format "%\n%\n%\n" job.sJobFile job.sPluginFile job.sMaxfile to:oFlags
	if job.bDependsOnPrevious then
	(
		format " -dependsonprevious\n" to:oFlags
	)
)

close oFlags

HiddenDOSCommand sCommand startpath:"C:\\Program Files\\Thinkbox\\Deadline7\\bin\\" exitcode:&ExitCode


aDeadlineQueue = #()

)

–This one uses cmd to enable DBR without needing a license, use the other one if possible
global SubmitToDeadlineQueue2
fn SubmitToDeadlineQueue2 iPriority:50 sJobName:unsupplied sComment:unsupplied sDepartment:“AMS” iFPC:1 iMachineLimit:0 bDeleteOnComplete:false bSuspended:false bDependsOnPrevious:false bLimitBatchSize:false iMaxBatchSize:20 bDBR:false iDBRServers:20 =
(
if sDeadlineQueuePath == undefined then
(
messagebox “Init the queue first!”
)
else
(

	--Create the job and plugin files
	iCounter = (getfiles (sDeadlineQueuePath + "*.job")).count
	
	sJobFile = (sDeadlineQueuePath + "Job" + FormatNumber 4 (iCounter+1) + ".job")
	sPluginFile = (sDeadlineQueuePath + "Plugin" + FormatNumber 4 (iCounter+1) + ".job")
	
	if sJobName == unsupplied then
	(
		if maxfilename == "" then
		(
			messagebox "Please save your file first!" title:"Davy warns"
			return false
		)
		else
		(
			sJobName = getfilenamefile maxfilename
		)
	)
	
	bNameAvailable = false
	iCounter = 0
	while not bNameAvailable do
	(
		HiddenDOSCommand ("deadlinecommand -getjobIdsfilter jobname=\"" + sJobName + "\" > c:\\DeadlineJobs.txt") startpath:"C:\\Program Files\\Thinkbox\\Deadline7\\bin\\"
		oTemp = openfile "c:\\DeadlineJobs.txt"
		sLine = ""
		try
		(
			sLine = readline oTemp
		)catch()
		try(close oTemp)catch()
		
		if sLine == "" then
		(
			bNameAvailable = true
		)
		else
		(
			if matchpattern sJobName pattern:("*_" + FormatNumber 3 iCounter) then
			(
				iCounter += 1
				sJobName = (substring sJobName 1 (sJobName.count - 3)) + FormatNumber 3 iCounter
			)
			else
			(
				iCounter += 1
				sJobName = sJobName + "_" + FormatNumber 3 iCounter
			)
		)
	)
	savemaxfile (maxfilepath + sJobName + ".max") quiet:true
	
	
	--JobFile
	submitInfoFile = CreateFile sJobFile
	
	if (submitInfoFile != undefined) then
	(
		format "Plugin=3dsCmd\n" to:submitInfoFile
		format "ForceReloadPlugin=false\n" to:submitInfoFile
		
		frames = "0"
		if rendtimetype == 1 then
		(
			frames = (currenttime.frame as integer) as string
		)
		else if rendtimetype == 2 then
		(
			frames = (animationrange.start.frame as integer) as string + "-" + (animationrange.end.frame as integer) as string
			if rendnthframe >1 then frames+= "step" + rendnthframe as string
		)
		else if rendtimetype == 3 then
		(
			frames = (rendstart.frame as integer) as string + "-" + (rendend.frame as integer) as string
			if rendnthframe >1 then frames+= "step" + rendnthframe as string
		)
		else if rendtimetype == 4 then
		(
			frames = rendpickupFrames
			if rendnthframe >1 then frames+= "step" + rendnthframe as string
		)
		chunkSize = iFPC
		if bDBR then
		(
			frames = "0-" + ((iDBRServers - 1) as string)
			chunkSize = 1
		)
		
		format "Frames=%\n"             frames         to:submitInfoFile
		format "ChunkSize=%\n"          chunkSize       to:submitInfoFile
		format "Priority=%\n"           iPriority      to:submitInfoFile
		format "Pool=%\n"               "ams"   to:submitInfoFile
		format "SecondaryPool=%\n"  "none"  to:submitInfoFile
		
		format "Name=%\n"               sJobName        to:submitInfoFile
		
		if sComment != unsupplied then
		(
			format "Comment=%\n"            sComment        to:submitInfoFile
		)
		format "Department=%\n"         sDepartment           to:submitInfoFile
		
		--format "DeleteOnComplete=%\n" chk_autoDelete.checked to:submitInfoFile
		if bDeleteOnComplete then
		(
			format "OnJobComplete=%\n" "Delete" to:submitInfoFile
		)
		if bSuspended then
		(
			format "InitialStatus=Suspended\n" to:submitInfoFile    
		)
		
		format "MachineLimit=%\n"       iMachineLimit  to:submitInfoFile
		
		outputFilenameIndex = 0
		
		-- If an output filename is specified, include it in the submit info file
		if rendSaveFile and rendOutputFilename != "" then
		(
			format "OutputDirectory0=%\n" (getFilenamePath rendOutputFilename) to:submitInfoFile
			format "OutputFilename0=%\n" ((getFilenameFile rendOutputFilename) + "####" + (getFilenameType rendOutputFilename)) to:submitInfoFile
			outputFilenameIndex = outputFilenameIndex + 1
		)
		
		-- Include render elements
		reManager = maxOps.GetCurRenderElementMgr()
		if reManager.getElementsActive() then
		(
			reCount = reManager.NumRenderElements()
			for i = 0 to reCount - 1 do
			(
				if classof (reManager.GetRenderElement i) != Missing_Render_Element_Plug_in do --ignore RE's that return as missing in the scene
				(
					reFilename = reManager.GetRenderElementFilename i
					if reFilename != undefined and reFilename != "" do --skip RE's if output file path is undefined or empty
					(                        
						format "OutputDirectory%=%\n" outputFilenameIndex (getFilenamePath reFilename) to:submitInfoFile
						format "OutputFilename%=%\n" outputFilenameIndex ((getFilenameFile reFilename) + "####" + (getFilenameType reFilename)) to:submitInfoFile                        
						outputFilenameIndex = outputFilenameIndex + 1
					)
				)
			)
		)
		
		close submitInfoFile
	)
	
	--PluginFile
	JobInfoFile = CreateFile sPluginFile
	if (JobInfoFile != undefined) then
	(
		version = ((maxVersion())[1] / 1000)
		if maxOps.productAppID == #viz then
		(
			format "Application=VIZ\n" to:JobInfoFile
			version = 2001 + version
		)
		else
		(
			format "Application=Max\n" to:JobInfoFile
			if version > 9 then
				version = 1998 + version
		)
		format "Version=%\n" version to:JobInfoFile
		
		try
		(
			VersionInfo = dotnetclass "System.Diagnostics.FileVersionInfo"
			MyMax = VersionInfo.GetVersionInfo (pathConfig.appendPath (pathConfig.GetDir #maxroot) "3dsmax.exe")
			format "SubmittedFromVersion=%\n" MyMax.FileVersion to:JobInfoFile	
		)catch()				
		
		format "Build=%\n" "64bit" to:JobInfoFile
	
		format "Camera=\n" to:JobInfoFile
		format "Camera0=\n" to:JobInfoFile
		
		format "PixelAspect=%\n" renderPixelAspect to:JobInfoFile
		format "ImageWidth=%\n" renderWidth to:JobInfoFile
		format "ImageHeight=%\n" renderHeight to:JobInfoFile
		format "ShowVFB=true\n" to:JobInfoFile
		format "ContinueOnError=true\n" to:JobInfoFile

		format "GammaCorrection=true\n" to:JobInfoFile
		format "GammaInput=%\n" fileingamma to:JobInfoFile
		format "GammaOutput=%\n" fileoutgamma to:JobInfoFile
		
		if rendSaveFile and rendOutputFilename != "" then
		(
			format "OutputFilename=%\n" rendOutputFilename to:JobInfoFile
		)
		
		format "SkipRenderedFrames=true\n" to:JobInfoFile

		local reMgr = maxOps.GetCurRenderElementMgr()
		if reMgr != undefined then
		(
			if reMgr.GetElementsActive() then
			(
				format "RenderElements=true\n" to:JobInfoFile
			)
			else
			(
				format "RenderElements=false\n" to:JobInfoFile
			)
		)
		
		if bDBR then
		(
			format "VRayDBRJob=true\n" to:JobInfoFile
			dbrFrame = (currenttime.frame as integer) as string
			format "DBRJobFrame=%\n" dbrFrame to:JobInfoFile
			format "LocalRendering=false\n" to:JobInfoFile
		)
		
		Close JobInfoFile
	)
	
	
	--Append the job to the queue
	oJob = DeadlineJob()
	oJob.sJobFile = sJobFile
	oJob.sPluginFile = sPluginFile
	oJob.sMaxfile = maxfilepath + maxfilename
	oJob.bDependsOnPrevious = bDependsOnPrevious
	
	append aDeadlineQueue oJob
	
	if bLimitBatchSize then
	(
		if aDeadlineQueue.count >= iMaxBatchSize then
		(
			FlushDeadlineQueue()
		)
	)
)

)

global SubmitToDeadlineQueue
fn SubmitToDeadlineQueue iPriority:50 sJobName:unsupplied sComment:unsupplied sDepartment:“AMS” iFPC:1 iMachineLimit:0 bDeleteOnComplete:false bSuspended:false bDependsOnPrevious:false bLimitBatchSize:false iMaxBatchSize:20 bDBR:false bTileRender:false iDBRServers:20 =
(
if sDeadlineQueuePath == undefined or SMTDFunctions == undefined then
(
messagebox “Init the queue first!”
)
else
(

	--Create the job and plugin files
	iCounter = (getfiles (sDeadlineQueuePath + "*.job")).count
	
	sJobFile = (sDeadlineQueuePath + "Job" + FormatNumber 4 (iCounter+1) + ".job")
	sPluginFile = (sDeadlineQueuePath + "Plugin" + FormatNumber 4 (iCounter+1) + ".job")
	
	if sJobName == unsupplied then
	(
		if maxfilename == "" then
		(
			messagebox "Please save your file first!" title:"Davy warns"
			return false
		)
		else
		(
			sJobName = getfilenamefile maxfilename
		)
	)
	
	bNameAvailable = false
	iCounter = 0
	while not bNameAvailable do
	(
		HiddenDOSCommand ("deadlinecommand -getjobIdsfilter jobname=\"" + sJobName + "\" > c:\\DeadlineJobs.txt") startpath:"C:\\Program Files\\Thinkbox\\Deadline7\\bin\\"
		oTemp = openfile "c:\\DeadlineJobs.txt"
		sLine = ""
		try
		(
			sLine = readline oTemp
		)catch()
		try(close oTemp)catch()
		
		if sLine == "" then
		(
			bNameAvailable = true
		)
		else
		(
			if matchpattern sJobName pattern:("*_" + FormatNumber 3 iCounter) then
			(
				iCounter += 1
				sJobName = (substring sJobName 1 (sJobName.count - 3)) + FormatNumber 3 iCounter
			)
			else
			(
				iCounter += 1
				sJobName = sJobName + "_" + FormatNumber 3 iCounter
			)
		)
	)
	savemaxfile (maxfilepath + sJobName + ".max") quiet:true
	
	SMTDFunctions.loadSettings()
	SMTDSettings.JobName = sJobName
	if sComment != unsupplied then
	(
		SMTDSettings.Comment = sComment
	)
	SMTDSettings.Priority = iPriority
	SMTDSettings.ChunkSize = iFPC
	if iMachineLimit > 0 then
	(
		SMTDSettings.LimitEnabled = true 
		SMTDSettings.MachineLimit = iMachineLimit
	)
	SMTDSettings.SubmitAsSuspended = bSuspended
	SMTDSettings.Department = sDepartment
	if bDeleteOnComplete then
	(
		SMTDSettings.OnComplete = "Delete"
	)
	else
	(
		SMTDSettings.OnComplete = "Nothing"
	)
	
	if bDBR then
	(
		SMTDSettings.DBR = true
		SMTDSettings.DBRServers = iDBRServers
	)
	else
	(
		SMTDSettings.DBR = false
		SMTDSettings.DBRServers = 1
	)
	
	if bTileRender then
	(
		SMTDSettings.RegionRenderingMode = #singleFrameTiles
		SMTDSettings.SingleTileJobDraft = true
		SMTDSettings.SingleTileJobCleanup = true
		SMTDSettings.SingleTileJobDependent = true
		SMTDSettings.TilesInY = iDBRServers
		SMTDSettings.TilesInX = iDBRServers
	)
	else
	(
		SMTDSettings.RegionRenderingMode = #none
		SMTDSettings.SingleTileJobDraft = false
		SMTDSettings.SingleTileJobCleanup = false
		SMTDSettings.SingleTileJobDependent = false
		SMTDSettings.TilesInY = 1
		SMTDSettings.TilesInX = 1
	)
	
	if bDependsOnPrevious then
	(
		
		SMTDSettings.SubmitAsDependent = true
	)
	else
	(
		SMTDSettings.SubmitAsDependent = false
	)

	SMTDFunctions.CreateSubmitInfoFile sJobFile
	SMTDFunctions.CreateJobInfoFile sPluginFile
	
	if bDBR then
	(
		oTempPluginFile = openfile sPluginFile mode:"a"
		format "VRayDBRJob=true\nDBRJobFrame=0\n" to:oTempPluginFile
		close oTempPluginFile
	)
	
	
	--Append the job to the queue
	oJob = DeadlineJob()
	oJob.sJobFile = sJobFile
	oJob.sPluginFile = sPluginFile
	oJob.sMaxfile = maxfilepath + maxfilename
	oJob.bDependsOnPrevious = bDependsOnPrevious
	
	append aDeadlineQueue oJob
	
	if bLimitBatchSize then
	(
		if aDeadlineQueue.count >= iMaxBatchSize then
		(
			FlushDeadlineQueue()
		)
	)
)

)[/code]

I’m sad to say I couldn’t figure it out. It feels like I should be copy pasting most of those 2 functions into my own, and then changing the final step to be compatible with the batch system, but that sounds like an awful way to do it, and on top of that, I’m not even sure how to get started.

I think the only other option is to abandon the batching system and just go for a straight up single job submitter and just use the SMTD functions, like you suggested. We often send out batches of 500 or even 1000+ renders, so I think that will be a major performance hit, though.

Any advice?

I am also sad to say that this portion of the submitter is the most demanding and drove us crazy during development. Supporting the V-Ray buffer and all its different output options, plus old vs. new OpenEXR plugin (Splutterfish vs. Cebas implementation) with or without Render Elements, keeping track of 5 region modes etc. made it very challenging.

However, I still think you should not abandon your batch system concept. In fact, I was contemplating something like a batch queue about a year ago, but dropped it, mainly because defining dependencies would be very very hard - you can only set a dependency once you know the ID of the job, and you cannot know it before the actual submission. So I would have to figure out a post-step to process dependencies within my queue.
But SMTD is already halfway there, since every submission goes to a separate date-stamped folder. So in theory I could just keep on creating the folders with all necessary files (and some control file containing all the settings at the time of submission), and then have another script or batch process walk through those folders and process them independently.

I will take a closer look at what you have there, and see if anything could be done on either side of the code…

Back in the office. I hope you had good holidays.

Dependencies were actually quite easy to fix in my batch script. Of course, we didn’t have as many options to support as you have. My goal was just to expose the options we need for our workflow, and gradually add new features when the need arises.

The DeadlineCommandbg.exe -multi way of submitting also allows for a -dependsonprevious flag, so I didn’t even need to look up the ID of the jobs. Whether the tile assembly part is compatible with this method, I don’t know…

Hey,

The -dependsonprevious flag should work for most cases. The issue that you could see with that is if you want to have one job dependent on multiple others since this method will make a chain of dependencies not a multi to one. An example of when you might want this is when doing region rendering with multiple frames.

Grant