To create C++ code implementation of a node, the user can use the node.json to C++ code template Python script to first create a template based on the node JSON file. In this section, we will discuss the C++ code structure in more detail. There are two types of nodes and, when creating one, you will inherit from one of these two interfaces:
In this document, we will focus mainly on process nodes. Process node covers anything that performs data processing. For instance, ISPNode would be a process node. When creating a node that is not based on a DriveWorks module, the developer is free to inherit from the abstract classes ProcessNode or SensorNode, but care must be taken to ensure that the node catches its own exceptions as there will be no guarantee of that. It is highly recommended to inherit from the Simple classes and ExceptionSafe classes.
For each node, there will be a public node interface and an implementation file. When creating a public node interface file, the interface should inherit from ExceptionSafeProcessNode
for a process node type. This is an exception catching layer so that the exceptions thrown in the implementation get translated into status codes. Upon construction, ExceptionSafeProcessNode requires a pointer to the implementation. Thus, when creating the HelloWorldNode
node, in the constructor call to the parent class ExceptionSafeProcessNode a pointer to HelloWorldNodeImpl
would be passed. An example can be found in public header of the object detector custom node, HelloWorldNode.hpp
.
When defining the public header for the node, each pass is defined with macro NODE_REGISTER_PASS()
. NODE_REGISTER_PASS()
macro definition is defined in file <dwcgf/node/SimpleNodeT.hpp>
. In HelloWorldNodeImpl.cpp
, each pass is defined in initPass method:
// post-process CPU pass NODE_REGISTER_PASS( "PROCESS"_sv, [this]() { return process(); });
We can also describe each pass using pass descriptor APIs to make pass visualization in DW Graph GUI Tool. Pass descriptor API’s are defined in file <dwcgf/pass/PassDescriptor.hpp>
static constexpr auto passes() { return describePassCollection( describePass("SETUP"_sv, DW_PROCESSOR_TYPE_CPU), describePass("PREPROCESS"_sv, DW_PROCESSOR_TYPE_GPU), describePass("INFERENCE"_sv, DW_PROCESSOR_TYPE_GPU), describePass("POSTPROCESS"_sv, DW_PROCESSOR_TYPE_CPU), describePass("TEARDOWN"_sv, DW_PROCESSOR_TYPE_CPU)); };
In addition to defining passes, each input and output must be defined with a name. In addition, ports can also be defined with portIndex function which is defined in <dwcgf/port/PortDescriptor.hpp>
. Port init can be done with MACROs defined in SimpleNodeT.hpp. An input port definition example in SumNode is listed below:
// Setup input ports. NODE_INIT_INPUT_PORT("VALUE_0"_sv);
Similar with Pass Descriptor, we can also describe each port using port descriptor APIs to make port visualization in DW Graph GUI Tool. Pass descriptor API’s are defined in file <dwcgf/pass/PortDescriptor.hpp>
.
static constexpr auto describeInputPorts() { using namespace dw::framework; return describePortCollection(); }; static constexpr auto describeOutputPorts() { using namespace dw::framework; return describePortCollection( DW_DESCRIBE_PORT(int, "VALUE_0"_sv, PortBinding::REQUIRED), DW_DESCRIBE_PORT(int, "VALUE_1"_sv, PortBinding::REQUIRED)); };
The last describe function needed is the describeParameters API. It is used to describe the parameters of the node. Below is an example code from HelloWorldNode code. The function describes the parameter "name" which is also specified in HelloWorldNode.node.json:
static constexpr auto describeParameters() { return describeConstructorArguments( describeConstructorArgument<HelloWorldNodeParams>( DW_DESCRIBE_PARAMETER( dw::core::FixedString<64>, "name"_sv, &HelloWorldNodeParams::name)), describeConstructorArgument<dwContextHandle_t>( DW_DESCRIBE_UNNAMED_PARAMETER( dwContextHandle_t))); }
There should be a constructor inside the public header that instantiates the node. Commonly a struct which is named <node_name>Params
is passed as a parameter and contains the desired parameters to configure the node. A constructor parameter of type dwContextHandle_t
is commonly passed separately. Here is an example:
HelloWorldNodeImpl(const HelloWorldNodeParams& params, const dwContextHandle_t ctx);
Besides the constructor, a node needs a static create function and registers the node using a macro in the cpp file. A simplest example is to utilize something like below:
dw::framework::create<Node>(ParameterProvider& provider);
When creating an implementation file, the file should inherit from the concrete class SimpleNode
for a process node type. Using a similar pattern as stated earlier, the name of the node implementation should be the name of the node plus "Impl" (e.g., "HelloWorldNodeImpl"). In the implementation file, you should store all of the unique_ptr’s to the input and output ports.
void ISPNodeImpl::initPasses() { // SETUP and TEARDOWN passes are using default impl NODE_REGISTER_PASS("PROCESS"_sv, [this]() { return process(); }); }
void ISPNodeImpl::initPorts() { // Setup input ports, waitTime is zero by default NODE_INIT_INPUT_PORT("IMAGE"_sv); // Setup output ports const auto& transParams = m_params.transParams; dwImageProperties imageProps{}; imageProps.width = transParams.inputRes.x; imageProps.height = transParams.inputRes.y; imageProps.type = DW_IMAGE_CUDA; imageProps.memoryLayout = DW_IMAGE_MEMORY_TYPE_PITCH; imageProps.format = DW_IMAGE_FORMAT_RGB_FLOAT16_PLANAR; // Create RGB (fp16) full resolution image FRWK_CHECK_DW_ERROR(dwImage_create(&m_convertedFullImage, imageProps, m_ctx)); imageProps.width = transParams.outputRes.x; imageProps.height = transParams.outputRes.y; NODE_INIT_OUTPUT_PORT("IMAGE"_sv, imageProps); NODE_INIT_OUTPUT_PORT("IMAGE_FOVEAL"_sv, imageProps); bool refSignal{}; NODE_INIT_OUTPUT_PORT("FOVEAL_SIGNAL"_sv, refSignal); }
Pass is the real meat behind the execution abstraction model because it provides the scheduler with timing information about each task and guarantees by defining each task is atomic with respect to the processor type it runs on. This theoretically allows a scheduler to efficiently schedule parallel work. When creating a pass, the heart of it is the function that is passed into NODE_REGISTER_PASS macro. The function will be passed a pointer to the node impl itself as the only parameter and should return a status. The user doesn’t need to explicitly write setup and teardown pass functions. These functions are implemented in the background. The setup pass gets executed first every time the node is executed, and the teardown gets called at the end of execution. The teardown is where all the outputs are sent. This is done by calling the member port output send method. Below is an example of the function passed into NODE_REGISTER_PASS macro in the HelloWorldNode:
dwStatus HelloWorldNodeImpl::process() { auto& outPort0 = NODE_GET_OUTPUT_PORT("VALUE_0"_sv); auto& outPort1 = NODE_GET_OUTPUT_PORT("VALUE_1"_sv); if (outPort0.isBufferAvailable() && outPort1.isBufferAvailable()) { *outPort0.getBuffer() = m_port0Value++; DW_LOGD << "[Epoch " << m_epochCount << "] Sent value0 = " << *outPort0.getBuffer() << Logger::State::endl; outPort0.send(); *outPort1.getBuffer() = m_port1Value--; DW_LOGD << "[Epoch " << m_epochCount << "] Sent value1 = " << *outPort1.getBuffer() << Logger::State::endl; outPort1.send(); } DW_LOGD << "[Epoch " << m_epochCount++ << "] Greetings from HelloWorldNodeImpl: Hello " << m_params.name.c_str() << "!" << Logger::State::endl; return DW_SUCCESS; }
A static LOG_TAG should also be provided to identify the node. This is a label used for logging inside the framework. It should be the same as the node name. For example, HelloWorldNode
would be:
static constexpr char LOG_TAG[] = "HelloWorldNode";
Any messages logged out within the node (e.g. a message of an exception) will be prepended by the LOG_TAG, therefore the message to be logged would suffice if it contains information in the following pattern: <function name>:<message>
. As a example, for an exception to be thrown, we can have the following message,
throw Exception(DW_NOT_IMPLEMENTED, "validate: not implemented");
The validate function by default is implemented in SimpleNodeT.hpp. The user can overwrite this function for any custom validate implementation. An example to use validate() would be to validate all the ports are bound to the appropriate channels (any required ports, that is). For example, a camera node may have processed output and raw output ports, but only one is required to be hooked up. A validate method can inspect the ports to make sure at least one is bound. In the ISPNode, we validate if the required inputPortImageHandle is available:
dwStatus ISPNodeImpl::validate() { dwStatus status = Base::validate(); // Check foveal ports are bound when foveal enabled if (status == DW_SUCCESS && (fovealEnabled() && (!NODE_GET_OUTPUT_PORT("IMAGE_FOVEAL"_sv).isBound() || !NODE_GET_OUTPUT_PORT("FOVEAL_SIGNAL"_sv).isBound()))) { return DW_NOT_READY; } return status; }
The reset function by default is implemented in SimpleNodeT.hpp. However, user can overwrite the reset function for any custom reset implementation.
dwStatus HelloWorldNodeImpl::reset() { m_port0Value = 0; m_port1Value = 10000; return Base::reset(); }
#include <dwcgf/node/NodeFactory.hpp> DW_REGISTER_NODE(dw::framework::HelloWorldNode)
src/dwframework/dwnodes
as well as src/cgf/nodes
)src/cgf/graphs/descriptions/nodes
src/cgf/graphs/descriptions/graphlets
)CGFDemo.graphlet.json
(file provided in src/cgf/graphs/descriptions/graphlets
) in JSON format which composed of nodes and graphletCGFDemo.graphlet.json
src/cgf/graphs/descriptions/systems
. Please refer to previous section for these metadata descriptionsUsing the provided tool in tools/descriptionScheduleYamlGenerator
, convert application description JSON files into YAML file for STM compiler
command: ./descriptionScheduleYamlGenerator.py --app CGFDemo.app.json --output CGFDemo__standardSchedule.yaml CGFDemo__slowSchedule.yaml
Using stmcompiler
tool from STM package, .stm
binary files can be generated with YAML inputs
commands:
./stmcompiler -i CGFDemo__standardSchedule.yaml -o CGFDemo__standardSchedule.stm
./stmcompiler -i CGFDemo__slowSchedule.yaml -o CGFDemo__slowSchedule.stm
CGFDemo__standardSchedule.stm
and CGFDemo__slowSchedule.stm
, into cgf/graphs
directory on DDPX. In addition, copy custom node directory and updated JSON files such as CGFDemo.graphlet.json
onto DDPXsudo ./run_cgf_demo.sh
Some demo components are released in binary form. Please refer to the description of each binary:
With the node structure explained above, we can now create a custom node based on the HelloWorld and Sum sample code provided. Please follow the following steps to add the HelloWorld and Sum sample node into the demo compute graph:
subcomponents
: "helloworld": { "componentType": "../../../nodes/HelloWorldNode.node.json", "parameters": { "name": "$name" } }, "sum": { "componentType": "../../../nodes/SumNode.node.json" }
parameters
, add HelloWorld name parameter: "name": { "type": "dw::core::FixedString<64>", "default": "Demo" }
connections
: { "src": "helloworld.VALUE_0", "dests": {"sum.VALUE_0": {}} }, { "src": "helloworld.VALUE_1", "dests": {"sum.VALUE_1": {}} },
standardSchedule
and slowSchedule
schedule, add demo nodes to the renderEpoch
passes: "renderEpoch": { "passes": [ "cgfDemo.arender", "cgfDemo.helloworld", "cgfDemo.sum" ] }
camera_pipeline0
process, add demo nodes to the list of subcomponents
: "subcomponents": [ "cgfDemo.cameraPipelineFront0", "cgfDemo.arender", "cgfDemo.helloworld", "cgfDemo.sum" ],
./descriptionScheduleYamlGenerator.py --app CGFDemo.app.json --output CGFDemo__standardSchedule.yaml CGFDemo__slowSchedule.yaml
Now generate STM binary with stmcompiler tool. Using stmcompiler in the tools folder, use commands:
./stmcompiler -i CGFDemo__standardSchedule.yaml -o CGFDemo__standardSchedule.stm
./stmcompiler -i CGFDemo__slowSchedule.yaml -o CGFDemo__slowSchedule.stm
To double check that the generated schedules contain the new nodes, the following commands should have matches:
grep "helloworld" CGFDemo__standardSchedule.stm -
grep "helloworld" CGFDemo__slowSchedule.stmTo quickly verify if the node has been added into the framework successfully, prints can be added in the C++ implementation. This example can be found in the HelloWorld node.