Compute Graph Framework SDK Reference  5.10
Implement a C++ Node

The dw::framework::Node interface prescribes a set of virtual functions which each node must implement. While not a requirement, the instructions apply the pointer to implementation (PIMPL) idiom to hide implementation details and reduce compilation dependencies.

The following subsections show how to implement a node named my_ns::MyNode.

Node Header

Base Class

The MyNode class needs to implement the dw::framework::Node interface. With the implementation being hidden in another class (called Impl) which is only known to the source file, all functions of the interface must be relayed to an Impl instance. During that relaying an additional functionality can be easily integrated: catching potential exceptions thrown in the Impl class and return dwStatus instead.

namespace my_ns
{
class MyNode : public dw::framework::ExceptionSafeProcessNode
{
    ...
}
} // namespace my_ns

Every node must define a set of static functions used to introspect the interface of the node as well as to instantiate a node. These static functions are documented in dw::framework::NodeConcept.

Instantiation (Declaration)

The node constructor can have arbitrary arguments. Commonly they are structured in the following way:

  • A struct containing the parameters which can't be changed after construction, e.g. struct MyNodeParams.
  • Other handles needed by the node, e.g. dwContextHandle_t.
    MyNode(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx);

A factory function enables the instantiation of any node in a uniform way. The parameter provider can retrieve the parameter value for all parameters declared in the describeParameters() function. The information from the describeParameters() function also allows to map these parameter values to the necessary constructor arguments and members within a constructor argument.

    static std::unique_ptr<MyNode> create(dw::framework::ParameterProvider&);

Introspection

Ports

All input/output ports are enumerated in a user defined order. For each port the unique name and C++ data type must be declared. Additionally, optional flags can be specified, e.g. if a port must be bound to a channel in order for the node to be consider to be in a valid state.

    static constexpr auto describeInputPorts()
    {
        return dw::framework::describePortCollection(
            DW_DESCRIBE_PORT(TypeFoo, "FOO"),
            DW_DESCRIBE_PORT(TypeBar, "BAR", dw::framework::PortBinding::REQUIRED),
            DW_DESCRIBE_PORT_ARRAY(TypeBaz, 3, "BAZ")
        );
    }

    static constexpr auto describeOutputPorts() {
        return dw::framework::describePortCollection(
            DW_DESCRIBE_PORT(TypeOut, "OUT"),
        );
    }

Passes

All passes are enumerated in their execution order. By convention the first pass is named SETUP and the last pass is named TEARDOWN. Beside the unique name the processor type where the computation is happening must be declared.

    static constexpr auto describePasses()
    {
        return dw::framework::describePassCollection(
            dw::framework::describePass("SETUP", DW_PROCESSOR_TYPE_CPU),
            dw::framework::describePass("PROCESS", DW_PROCESSOR_TYPE_GPU),
            dw::framework::describePass("AGGREGATE", DW_PROCESSOR_TYPE_CPU),
            dw::framework::describePass("TEARDOWN", DW_PROCESSOR_TYPE_CPU),
        );
    }

Parameters

All parameters declare a mapping to constructor arguments or members within a constructor argument. While the constructor arguments must be declared in the same order as they appear in the constructor signature, the parameters within each are enumerated in a user defined order.

    static constexpr auto describeParameters()
    {
        return dw::framework::describeConstructorArguments<
            MyNodeParams,
            MyNodeRuntimeParams,
            dwContextHandle_t,
        >(
            dw::framework::describeConstructorArgument(
                DW_DESCRIBE_..._PARAMETER(...),
                DW_DESCRIBE_..._PARAMETER(...),
                ...
                DW_DESCRIBE_..._PARAMETER(...)
            ),
            dw::framework::describeConstructorArgument(
                ...
            ),
            dw::framework::describeConstructorArgument(
                ...
            )
        );
    }

There are various kinds of parameters and for each there is a corresponding macro. All of these macros start with DW_DESCRIBE_ and end with PARAMETER.

For all parameters the C++ type needs to be specified as the first argument which must match the variable type the value will be stored in. Optionally a semantic type can be provided (see "Parameter Details" under Details/Node).

The destination where the parameter value should be stored is specified at the end of the argument list.

  • Commonly a single destination argument is used to provide the member pointer within the struct (see describeConstructorArgument(DW_DESCRIBE_PARAMETER(..., &HelloWorldNodeParams::name)) in HelloWorldNode.hpp).
  • Multiple destination arguments can be used to identify a nested member, each argument being a member point of the struct in each level. E.g. for two destination arguments like DW_DESCRIBE_PARAMETER(..., &MyNodeParams::memberWhichIsAStruct, &MyNodeParamsSubStruct::nestedMember) the value is stored in the nested member constructorArgument.memberWhichIsAStruct.nestedMember.
  • No destination arguments are specified when: either the parameter is the only parameter representing a constructor argument and the constructor argument itself should be the parameter value (see describeConstructorArgument(DW_DESCRIBE_UNNAMED_PARAMETER(dwContextHandle_t)) in HelloWorldNode.hpp) or for ABSTRACT parameters where the destination is determined by custom logic.

The signature of each of the following macros can be discovered by following the documentation of the macro to the function which is invoked by the macro.

Node Source

Enum Description

Beside primitive types or arrays thereof, a parameter can also be of enum type. For such an enum type a mapping must exist which correlates the integer value with a string representation. Commonly the identifier of each enumerator is being used. To define the mapping mentioned in the previous section, a specialization of the templated struct dw::framework::EnumDescription() needs to be defined.

For C enums this specialization can be places in the .cpp of the concrete node using a parameter with that enum type.

template <>
struct EnumDescription<MyEnum>
{
    static constexpr auto get()
    {
        return describeEnumeratorCollection<MyEnum>(
            DW_DESCRIBE_C_ENUMERATOR(NAME_OF_ENUMERATOR_1),
            DW_DESCRIBE_C_ENUMERATOR(NAME_OF_ENUMERATOR_2),
            ...
            DW_DESCRIBE_C_ENUMERATOR(NAME_OF_ENUMERATOR_N)
        );
    }
};

For C++ enum classes this specialization should not be placed in the nodes sources but wherever the enum class definition is located.

template <>
struct EnumDescription<MyEnum>
{
    static constexpr auto get()
    {
        using EnumT = MyEnum;
        return describeEnumeratorCollection<EnumT>(
            DW_DESCRIBE_ENUMERATOR(NAME_OF_ENUMERATOR_1),
            DW_DESCRIBE_ENUMERATOR(NAME_OF_ENUMERATOR_2),
            ...
            DW_DESCRIBE_ENUMERATOR(NAME_OF_ENUMERATOR_N)
        );
    }
};

Instantiation (Definition)

The base class constructor expects a unique_ptr to a node. As such an instance of the Impl class is passed which commonly has the same constructor signature as the concrete node.

MyNode::MyNode(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx)
    : dw::framework::ExceptionSafeNode(std::make_unique<MyNodeImpl>(params, runtimeParams, ctx))
{}

For the factory functions there are two implementation options:

  1. If the mapping of declared parameters to constructor arguments (members) is 1-to-1 without the need for custom logic, it just relays the call to the function dw::framework::create() which implements the mapping generically.
    std::unique_ptr<MyNode> MyNode::create(dw::framework::ParameterProvider& provider)
    {
        return dw::framework::create<MyNode>(provider);
    }
    
  2. If custom logic is needed it can be implemented between the calls dw::framework::createConstructorArguments() and dw::framework::makeUniqueFromTuple():
    std::unique_ptr<MyNode> MyNode::create(dw::framework::ParameterProvider& provider)
    {
        auto constructorArguments = dw::framework::createConstructorArguments<MyNode>();
    
        // to access individual constructor arguments by index:
        MyNodeParams& params = std::get<0>(constructorArguments);
        MyNodeRuntimeParams& runtimeParams = std::get<1>(constructorArguments);
        // arbitrary logic
        // e.g. reading two abstract parameters and using them together to populate the constructor argument
        size_t index;
        provider.getRequired("index", &index);  // get an index from the JSON config file
        provider.getRequired("enabled", index, &(params.enable));  // extract a single flag from an array of flag using the index
    
        dw::framework::populateParameters<MyNode>(constructorArguments, provider);
    
        return dw::framework::makeUniqueFromTuple<MyNode>(std::move(constructorArguments));
    }
    

Discovery

Registering the node type allows it to be discovered and instantiated by its string name.

The registration with DW_REGISTER_NODE() should happen outside of the namespace and must use the fully qualified typename.

namespace my_ns
{
    ...
}

DW_REGISTER_NODE(my_ns::MyNode)

Impl Header

Impl Base Class

The implementation class should inherit from a base class which not only implements the node interface but provides a default implementation for various functions. While the base class dw::framework::SimpleNode() provides all the necessary functionality, subclassing from dw::framework::SimpleNodeT() enables the usage of various macros later which can utilize the type of the node class.

class MyNodeImpl : public dw::framework::SimpleNodeT<MyNode>
{
    ...
}

Constructor and Initialization (Declaration)

The class should have a constructor with the same signature as the non-impl class.

    MyNodeImpl(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx);

In order to provide default implementations with rich functionality for various functions, the dw::framework::SimpleNode() class needs to know about the ports and passes of the node. Those need to be initialize / registered and by convention that happens in a function init() which will be called in the constructor.

    void init();

Pass Methods

Additionally, for each pass (except the conventional ones SETUP and TEARDOWN) a method is declared to implement the logic of that pass. The methods can't take any argument, instead information can be passed between passes using member variables of the class.

    dwStatus processPass();
    dwStatus aggregatePass();

Impl Source

Constructor and Initialization (Definition)

The constructor of the Impl class commonly stores the constructor arguments in member variables for later usage.

MyNodeImpl::MyNodeImpl(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx)
    : m_params(params), m_runtimeParams(runtimeParams), m_ctx(ctx)
{
    init();
}

For the dw::framework::SimpleNode() base class to function properly, in the init() function:

void MyNodeImpl::init()
{
    NODE_INIT_INPUT_PORT("FOO"_sv);
    NODE_INIT_INPUT_PORT("BAR"_sv);
    NODE_INIT_INPUT_ARRAY_PORT("BAZ"_sv);

    TypeOut ref{};
    NODE_INIT_OUTPUT_PORT("OUT"_sv, ref);

    NODE_REGISTER_PASS("PROCESS"_sv, [this]() {
        return processPass();
    });
    NODE_REGISTER_PASS("AGGREGATE"_sv, [this]() {
        return aggregatePass();
    });
}

There is no need for the concrete impl class to contain any member variables for dw::framework::Port instance. Instead, these can be retrieved from the base class using one of the following macros by passing the port name and for array port additionally the index:

Similarly, there is no need to store dw::framework::Pass instances in member variables in the concrete node.

Pass Implementation

Implementation of the pass methods declared in the header with the desired logic.

Generating Node Descriptor

The nodedescriptor tool can be used to generate the MyNode.node.json file based on the information provided by the introspection API.