This exercise assumes you have completed the previous exercise.
The build runs and prints the build log to the output window.
--- <Building Project 'D:\Release6\Content\Library\Octopus\Western-Washington-Mt-Baker-Terrain-Analysis\Western-Washington-Mt-Baker-Terrain-Analysis.box'> ---
Doing build work...
Build completed.
Let's take a look at the build command and figure out the implementation.
///////////////////////////////////////////////////////////////////////////////
// macro
///////////////////////////////////////////////////////////////////////////////
function void OctopusExecuteBuild_OnUpdate( CommandPresentationModuleInfo commandInfo )
{
commandInfo.Status.SetHint( "Builds this document" );
}
macro OctopusExecuteBuild( CommandPresentationModuleInfo commandInfo )
[Category="Build Commands", Package="Scenome-Package-App-Octopus", Guid="{D847E203-1072-4100-B5A7-2E745923D573}", Image=".\\icons\\generic_script_icon.bmp"]
{
if( LibAppServiceSettings.GetBoolParam( "ClearOutputWindowBeforeBuild" ) )
{
Console.Clear();
}
auto StrList a_slMessages;
a_slMessages.AddBlank();
LibAppOctopus.GenerateProjectBuildHeader(
Model.Filename, a_slMessages );
// Do the build here...
LibAppOctopus.Build( a_slMessages );
a_slMessages.AddBlank();
a_slMessages.Add( "Build completed." );
LibAppOctopus.Out( a_slMessages );
}
So far the build process only calls LibAppOctopus.Build( a_slMessages );, prints the build log, and then exits.
// Do the build here...
Render3D a_oAccel = Application.GetAccelerate3D();
auto RenderInfo a_oRenderInfo = new RenderInfo( a_oAccel );
// Get the GLSL compiler version.
// Use the highest version supported by your GPU.
int a_eMinGlslVersion = Enum.ShadingLanguageVersion_430();
bool a_bCheckCompiler = LibRender3D.CheckGlslCompiler(
a_oAccel, a_eMinGlslVersion, a_slMessages );
if( !( a_bCheckCompiler ) )
{
string a_sMessage =
"GLSL compiler is NOT at least #version " + a_eMinGlslVersion;
a_slMessages.Add( a_sMessage );
a_slMessages.AddBlank();
a_slMessages.Add( "Build failed!" );
LibAppServiceBuild.Out( a_slMessages );
return;
}
// Collect the WorkItemNodes from the document.
auto NodeBuffer a_apJobs;
auto NodeQuery a_oQuery;
a_oQuery.QueryNode( Model, a_apJobs, WorkItemNode );
// DEBUG
//LibNodeBuffer.Out( a_apJobs );
if( a_apJobs.GetCount() )
{
LibAppOctopus.Build( a_apJobs, commandInfo, a_slMessages );
}
else
{
string a_sMessage =
"No <WorkItemNode> objects found in document. Nothing to build.";
a_slMessages.Add( a_sMessage );
}
First we check that the GLSL compiler is at least #version 430, which is the minimum version with compute shader support. If the version is less than 430, we print a build output message and return without doing any work. It's possible to have compute shader support for versions less than 430. If this is the case on your computer, then feel free to change the minimum version here to whatever will work for you.
Then we use a <NodeQuery> object to populate a <NodeBuffer> object with pointers to all the <WorkItemNode> objects in the document. The <NodeBuffer> object is like a std::vector of pointers, but you can remove items from the collection without causing the collection to be resized. The <NodeQuery> object is a pretty simple as well: it allows you to search the hierarchy (using breadth first search) with a variety of filtering options, such as node type or other criteria.
After collecting the <WorkItemNode> objects, the code invokes the build function for the collection. We pass to the following items to the build function: the <NodeBuffer> object representing the complete set of CPU or GPU compute workloads, the <CommandPresentationModuleInfo> object so we can update the status bar with messages during the build if desired, and a <StrList> object that stores log messages generated during the build.
Separation-of-concerns is critical here, and throughout this implementation. We do not want the build system to know anything about the workloads it executes.
///////////////////////////////////////////////////////////////////////////////
// function
///////////////////////////////////////////////////////////////////////////////
function void Out( StrList p_slMessages )
{
for( int i = 0; i < p_slMessages.GetCount(); ++i )
{
Console.Out( p_slMessages.GetAt( i ) );
}
}
We're going to implement a function that calls a script remotely. The remote script is what will actually perform the CPU and GPU compute workload. We must maintain a very clear separation-of-concerns here, so that these build system knows nothing about the workload it executes. That way we can scale this system out to perform a wide variety of CPU and GPU compute workloads.
///////////////////////////////////////////////////////////////////////////////
// function
///////////////////////////////////////////////////////////////////////////////
function bool CallBuildFunction(
ScriptFunction p_oTarget,
WorkItemNode p_oJob,
CommandPresentationModuleInfo p_oInfo,
StrList p_slMessages
)
{
// Update the status bar...
p_oInfo.Status.SetHint(
"Executing workload on '" + p_oJob.Name + "'" );
// Configure the objects we're passing
// as function parameters.
auto VariantArray a_oParams;
auto Variant a_oReturnValue;
int a_nReturnVal = 0;
int a_nParamCount = 3;
a_oParams.Count = a_nParamCount;
a_oParams.Objects[ 0 ].SetObject( p_oJob, WorkItemNode );
a_oParams.Objects[ 1 ].SetObject( p_oInfo, CommandPresentationModuleInfo );
a_oParams.Objects[ 2 ].SetObject( p_slMessages, StrList );
// Call the target function.
bool a_bCallStatus = p_oTarget.Call(
Script, a_oReturnValue, a_oParams );
if( a_bCallStatus )
{
p_slMessages.Add(
"Successfully executed workload named '" +
p_oJob.Name + "'." );
}
else
{
p_slMessages.Add(
"Failed to execute workload named '" +
p_oJob.Name + "'." );
}
return true;
}
This code starts out setting the status bar text to indicate the current workload. Then we pack the function parameters into a <VariantArray> so that we can pass them to the remote function call. Using a remote function call here means that the function signature is the only detail that this code needs to know about the code that executes the CPU and GPU compute workloads. Furthermore, using a remote function call means the build system doesn't know that the workload execution code even exists. At the end, we append the build messages with a message indicating success or failure.
///////////////////////////////////////////////////////////////////////////////
// function
///////////////////////////////////////////////////////////////////////////////
function bool Build( StrList p_slMessages )
{
p_slMessages.Add( "Doing build work..." );
return true;
}
We're going to implement a new function.
///////////////////////////////////////////////////////////////////////////////
// function
///////////////////////////////////////////////////////////////////////////////
function bool Build( NodeBuffer p_apJobs, CommandPresentationModuleInfo p_oInfo, StrList p_slMessages )
{
// Invoke the build command for each WorkItemNode.
for( int i = 0; i < p_apJobs.GetCount(); ++i )
{
WorkItemNode a_oJob = (WorkItemNode)p_apJobs.Get( i );
p_slMessages.AddBlank();
// Skip invisible WorkItemNode objects.
if( !( a_oJob.Visible ) )
{
p_slMessages.Add( " named '" + a_oJob.Name +
"' is invisible and will be skipped." );
continue;
}
// Return failure if there are no child nodes.
if( !( a_oJob.ChildCount ) )
{
p_slMessages.Add( "<WorkItemNode> named '" + a_oJob.Name +
"' contains no children! Unable to execute empty workload." );
continue;
}
// Resolve the path to the script that performs the CPU or GPU compute
// workload. This path comes in to this function as a relative path
// such as '..\\..\\folder\\file.ssl' and we need an absolute path.
auto FilePath a_oScriptPath = new FilePath( a_oJob.ScriptPath );
a_oScriptPath.ResolveToModel( a_oJob );
// Make sure we can actually load the script.
// There might be SSL compiler errors.
// This is going to compile the script code 'just in time'.
// If the script fails to compile, the script engine
// will print the errors in the output window, even
// though you don't see code for it here.
ScriptSource a_oSource = ScriptSource.LoadFromFile(
a_oScriptPath.GetPath() );
if( !( a_oSource ) )
{
string a_sMessage = "Unable to load script " +
"specified by <WorkItemNode>: " + a_oScriptPath.GetPath();
p_slMessages.Add( a_sMessage );
delete a_oSource;
continue;
}
// Find the actual build function.
string a_sFunc = a_oJob.ScriptFunction;
ScriptFunction a_oTarget = a_oSource.FindFunction( a_sFunc );
if( !( a_oTarget ) )
{
string a_sMessage = "Unknown function: " +
"specified by <WorkItemNode>: " + a_sFunc;
p_slMessages.Add( a_sMessage );
delete a_oSource;
continue;
}
p_oInfo.Status.SetHint( "Building <WorkItemNode> '" + a_oJob.Name + "'" );
CallBuildFunction( a_oTarget, a_oJob, p_oInfo, p_slMessages );
delete a_oSource;
}
p_oInfo.Status.SetHint( "Build complete" );
return true;
}
This code starts out by iterating over the collection of <WorkItemNode> objects. The code skips any <WorkItemNode> objects marked as invisible, and updates the build log. Then the code checks to see if there are any child nodes. Recall that earlier we add two child nodes when we cretae each <WorkItemNode>.
After performing the initial tests, we convert the relative path specified by the <WorkItemNode> to an absolute path. We need an absolute path because we have to open the script and compile it. This is the script that will execute the workoad.
After creating an absolute path, we try to load the script from disk. The script file will be compiled during this process, and any script errors will be printed in the output window. Typically scripts are compiled when you select Desktop » Refresh Scripts from the main menu. In this case, because we must use a remote script call to maintain separation-of-concerns, we must also use just-in-time compilation. This can be a little difficult at first because you won't discover script errors until the build runs, but in practice it works quite well.
Once the script is compiled, we search for the script function specified by the <WorkItemNode>, and print a build message if it is not found. In this case, although it is a serious error, we'll call continue so the loop keeps running while we try to see if we can successfully build anything.
Last, we call CallBuildFunction( ... ), which executes the remote function call.
It's a little wasteful to reload and recompile the script each time, but we don't have much choice here since each <WorkItemNode> can define its own workloads. It might be useful to sort workloads later, but we won't worry about that for now.
///////////////////////////////////////////////////////////////////////////////
// function
///////////////////////////////////////////////////////////////////////////////
function bool Execute( WorkItemNode p_oJob, CommandPresentationModuleInfo p_oInfo, StrList p_slMessages )
{
p_slMessages.Add( "Executed build..." );
return true;
}
We're going to modify this function to perform the build.
///////////////////////////////////////////////////////////////////////////////
// function
///////////////////////////////////////////////////////////////////////////////
function bool Execute( WorkItemNode p_oWork, CommandPresentationModuleInfo p_oInfo, StrList p_slMessages )
{
auto FilePath a_oAbsPath = new FilePath( p_oWork.ScriptPath );
p_slMessages.Add( "Remote script execution succeeded: " + a_oAbsPath.GetPath() );
p_slMessages.AddBlank();
return true;
}
This function is just a stub, but it allows us to check if the remote function call is successful.
If there are any script compiler errors, undo your changes in the text editor, go back to the previous step, and follow the instructions again. Here is an example of what error messages might look like:
Start loading scripts
D:\release6\scripts\app_shell_util.ssl(1770) : error: newline in constant
Done loading scripts; 10 loaded in 1.29 ms; avg 0.09
The build runs and prints the build log to the output window.
--- <Building Project 'D:\Release6\Content\Library\Octopus\Western-Washington-Mt-Baker-Terrain-Analysis\Western-Washington-Mt-Baker-Terrain-Analysis.box'> ---
GLSL compiler version: 460
Remote script execution: ..\..\..\..\scripts\app_octopus_workload_build_array_texture_from_geotiff_util.ssl
Successfully executed workload named 'BuildArrayTextureFromGeotiff'.
Build completed.
The message we printed is visible in the build log.
Remote script execution: ..\..\..\..\scripts\app_octopus_workload_build_array_texture_from_geotiff_util.ssl
The build command now works correctly. Next, we'll do the complete implementation of this function so that it executes a workload.
This exercise is complete. Please proceed to the next exercise.