Tutorial 3 - ABI Override Node

Although the .ogn format creates an easy-to-use interface to the ABI of the OmniGraph node and the associated data model, there may be cases where you want to override the ABI to perform special processing.

OgnTutorialABI.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.Abi”, in its first version, with a simple description. The single attribute serves mostly to provide a framework for the ABI discussion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
    "Abi" : {
        "$comment": [
            "Any key of a key/value pair that starts with a dollar sign, will be ignored by the parser.",
            "Its values can be anything; a number, string, list, or object. Since JSON does not have a",
            "mechanism for adding comments you can use this method instead."
        ],
        "version": 1,
        "categories": ["tutorials", { "internal:abi": "Internal nodes that override the ABI functions" }],
        "exclude": ["python"],
        "description": ["This tutorial node shows how to override ABI methods on your node."],
        "metadata": {
            "$comment": "Metadata is key/value pairs associated with the node type.",
            "$specialNames": "Kit may recognize specific keys. 'uiName' is a human readable version of the node name",
            "uiName": "Tutorial Node: ABI Overrides"
        },
        "inputs": {
            "$comment": "Namespaces inside inputs or outputs are possible, similar to USD namespacing",
            "namespace:a_bool": {
                "type": "bool",
                "description": ["The input is any boolean value"],
                "default": true
            }
        },
        "outputs": {
            "namespace:a_bool": {
                "type": "bool",
                "description": ["The output is computed as the negation of the input"],
                "default": true
            }
        },
        "tests": [ {"inputs:namespace:a_bool": true, "outputs:namespace:a_bool": false} ]
    }
}

OgnTutorialABI.cpp

The cpp file contains the implementation of the node class with every possible ABI method replaced with customized processing. The node still functions the same as any other node, although it is forced to write a lot of extra boilerplate code to do so.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto.  Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#include <OgnTutorialABIDatabase.h>
using omni::graph::core::Token;

#include <string>
#include <unordered_map>

// Set this value true to enable messages when the ABI overrides are called
const bool debug_abi{ false };
#define DEBUG_ABI if (debug_abi) CARB_LOG_INFO

// In most cases the generated compute method is all that a node will need to implement. If for
// some reason the node wants to access the omni::graph::core::INodeType ABI directly and override
// any of its behaviour it can. This set of functions shows how every method in the ABI might be
// overridden.
//
// Note that certain ABI functions, though listed, are not implemented as a proper implementation would be more
// complicated than warranted for this simple example. All are rarely overridden; in fact no known cases exist.
class OgnTutorialABI
{
    static std::unordered_map<std::string, std::string> s_metadata; // Alternative metadata implementation
public:
    // ----------------------------------------------------------------------
    // Almost always overridden indirectly
    //
    // ABI compute method takes the interface definitions of both the evaluation context and the node.
    // When using a .ogn generated class this function will be overridden by the generated code and the
    // node will implement the generated version of this method "bool compute(OgnTutorialABIDatabase&)".
    // Overriding this method directly, while possible, does not add any extra capabilities as the two
    // parameters are also available through the database (contextObj = db.abi_context(), nodeObj = db.abi_node())
    static bool compute(const GraphContextObj& contextObj, const NodeObj& nodeObj)
    {
        DEBUG_ABI("Computing the ABI node");

        // The computation still runs the same as for the standard compute method by using the more
        // complex ABI-based access patterns. First get the ABI-compliant structures needed.
        NodeContextHandle nodeHandle = nodeObj.nodeContextHandle;
        const INode* iNode = nodeObj.iNode;

        // Use the multiple-input template to access a pointer to the input boolean value
        auto inputValueAttr = getAttributesR<ConstAttributeDataHandle>(
            contextObj, nodeHandle, std::make_tuple(Token("inputs:namespace:a_bool")));
        const bool* inputValue{ nullptr };
        std::tie(inputValue) = getDataR<bool*>(contextObj, inputValueAttr);

        // Use the single-output template to access a pointer to the output boolean value
        auto outputValueAttr = getAttributeW(contextObj, nodeHandle, Token("outputs:namespace:a_bool"));
        bool* outputValue = getDataW<bool>(contextObj, outputValueAttr);

        // Since the generated code isn't in control you are responsible for your own error checking
        if (!inputValue)
        {
            // Not having access to the generated database class with its error interface we have to resort
            // to using the native error logging.
            CARB_LOG_ERROR("Failed compute on %s: No input attribute", iNode->getPrimPath(nodeObj));
            return false;
        }
        if (!outputValue)
        {
            CARB_LOG_ERROR("Failed compute on %s: No output attribute", iNode->getPrimPath(nodeObj));
            return false;
        }

        // The actual computation of the node
        *outputValue = !*inputValue;

        return true;
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // These will almost never be overridden, specifically because the low level implementation lives in
    // the omni.graph.core extension and is not exposed through the ABI so it would be very difficult
    // for a node to be able to do the right thing when this function is called.
    // static void addInput
    // static void addOutput
    // static void addState

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // This should almost never be overridden as the auto-generated code will handle the name.
    // This particular override is used to bypass the unique naming feature of the .ogn name, where only names
    // with a namespace separator (".") do not have the extension name added as a prefix to the unique name.
    // This should only done for backward compatibility, and only until the node type name versioning is available.
    // Note that when you do this you will still be able to create a node using the generated node type name, as
    // that is required in order for the automated testing to work. The name returned here can be thought of as an
    // alias for the underlying name.
    static const char* getNodeType()
    {
        DEBUG_ABI("ABI override of getNodeType");
        static const char* _nodeType{ "OmniGraphABI" };
        return _nodeType;
    }

    // ----------------------------------------------------------------------
    // Occasionally overridden
    //
    // When a node is created this will be called
    static void initialize(const GraphContextObj&, const NodeObj&)
    {
        DEBUG_ABI("ABI override of initialize");
        // There is no default behaviour on initialize so nothing else is needed for this tutorial to function
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // This method might be overridden to set up initial conditions when a node type is registered, or
    // to replace initialization if the auto-generated version has some problem.
    static void initializeType(const NodeTypeObj& nodeType)
    {
        DEBUG_ABI("ABI override of initializeType");
        // The generated initializeType will always be called so nothing needs to happen here
    }

    // ----------------------------------------------------------------------
    // Occasionally overridden
    //
    // This is called while registering a node type. It is used to initialize tasks that can be used for
    // making compute more efficient by using Realm events.
    static void registerTasks()
    {
        DEBUG_ABI("ABI override of registerTasks");
    }

    // ----------------------------------------------------------------------
    // Occasionally overridden
    //
    // After a node is removed it will get a release call where anything set up in initialize() can be torn down
    static void release(const NodeObj&)
    {
        DEBUG_ABI("ABI override of release");
        // There is no default behaviour on release so nothing else is needed for this tutorial to function
    }

    // ----------------------------------------------------------------------
    // Occasionally overridden
    //
    // This is something you do want to override when you have more than version of your node.
    // In it you would translate attribute information from older versions into the current one.
    static bool updateNodeVersion(const GraphContextObj&, const NodeObj&, int oldVersion, int newVersion)
    {
        DEBUG_ABI("ABI override of updateNodeVersion from %d to %d", oldVersion, newVersion);
        // There is no default behaviour on updateNodeVersion so nothing else is needed for this tutorial to function
        return oldVersion < newVersion;
    }

    // ----------------------------------------------------------------------
    // Occasionally overridden
    //
    // When there is a connection change to this node which results in an extended type attribute being automatically
    // resolved, this callback gives the node a change to resolve other extended type attributes. For example a generic
    // 'Increment' node can resolve its output to an int only after its input has been resolved to an int. Attribute
    // types are resolved using IAttribute::setResolvedType() or the utility functions such as
    // INode::resolvePartiallyCoupledAttributes()
    static void onConnectionTypeResolve(const NodeObj& nodeObj)
    {
        DEBUG_ABI("ABI override of onConnectionTypeResolve()");
        // There is no default behaviour on onConnectionTypeResolve so nothing else is needed for this tutorial to
        // function
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // You may want to provide an alternative method of metadata storage.
    // This method can be used to intercept requests for metadata to provide those alternatives.
    static size_t getAllMetadata(const NodeTypeObj& nodeType, const char** keyBuf, const char** valueBuf, size_t bufSize)
    {
        DEBUG_ABI("ABI override of getAllMetadata(%zu)", bufSize);
        if (s_metadata.size() > bufSize)
        {
            CARB_LOG_ERROR("Not enough space for metadata - needed %zu, got %zu", s_metadata.size(), bufSize);
            return 0;
        }
        size_t index = 0;
        for (auto& metadata : s_metadata)
        {
            keyBuf[index] = metadata.first.c_str();
            valueBuf[index] = metadata.second.c_str();
            ++index;
        }
        return s_metadata.size();
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // You may want to provide an alternative method of metadata storage.
    // This method can be used to intercept requests for metadata to provide those alternatives.
    static const char* getMetadata(const NodeTypeObj& nodeType, const char* key)
    {
        DEBUG_ABI("ABI override of getMetadata('%s')", key);
        auto found = s_metadata.find(std::string(key));
        if (found != s_metadata.end())
        {
            return (*found).second.c_str();
        }
        return nullptr;
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // You may want to provide an alternative method of metadata storage.
    // This method can be used to intercept requests for the number of metadata elements to provide those alternatives.
    static size_t getMetadataCount(const NodeTypeObj& nodeType)
    {
        DEBUG_ABI("ABI override of getMetadataCount()");
        return s_metadata.size();
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // You may want to provide an alternative method of metadata storage.
    // This method can be used to intercept requests to set metadata and use those alternatives.
    static void setMetadata(const NodeTypeObj& nodeType, const char* key, const char* value)
    {
        DEBUG_ABI("ABI override of setMetadata('%s' = '%s')", key, value);
        s_metadata[std::string(key)] = std::string(value);
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // Subnode types are used when a single node type is shared among a set of other nodes, e.g. the way that
    // PythonNode is the node type for all nodes implemented in Python, while the subNodeType is the actual node type.
    // Overriding these functions let you implement different methods of managing those relationships.
    //
    static void addSubNodeType(const NodeTypeObj& nodeType, const char* subNodeTypeName, const NodeTypeObj& subNodeType)
    {
        DEBUG_ABI("ABI override of addSubNodeType('%s')", subNodeTypeName);
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // Subnode types are used when a single node type is shared among a set of other nodes, e.g. the way that
    // PythonNode is the node type for all nodes implemented in Python, while the subNodeType is the actual node type.
    // Overriding these functions let you implement different methods of managing those relationships.
    //
    static NodeTypeObj getSubNodeType(const NodeTypeObj& nodeType, const char* subNodeTypeName)
    {
        DEBUG_ABI("ABI override of getSubNodeType('%s')", subNodeTypeName);
        return NodeTypeObj{ nullptr, kInvalidNodeTypeHandle };
    }

    // ----------------------------------------------------------------------
    // Rarely overridden
    //
    // Subnode types are used when a single node type is shared among a set of other nodes, e.g. the way that
    // PythonNode is the node type for all nodes implemented in Python, while the subNodeType is the actual node type.
    // Overriding these functions let you implement different methods of managing those relationships.
    //
    static NodeTypeObj createNodeType(const char* nodeTypeName, int version)
    {
        DEBUG_ABI("ABI override of createNodeType('%s')", nodeTypeName);
        return NodeTypeObj{ nullptr, kInvalidNodeTypeHandle };
    }
};
std::unordered_map<std::string, std::string> OgnTutorialABI::s_metadata;

// ============================================================
// The magic for recognizing and using the overridden ABI code is in here. The generated interface
// is still available, as seen in the initializeType implementation, although it's not required.
REGISTER_OGN_NODE()

Node Type Metadata

This file introduces the metadata keyword, whose value is a dictionary of key/value pairs associated with the node type that may be extracted using the ABI metadata functions. These are not persisted in any files and so must be set either in the .ogn file or in an override of the initializeType() method in the node definition.

Exclusions

Note the use of the exclude keyword in the .ogn file. This allows you to prevent generation of any of the default files. In this case, since the ABI is handling everything the Python database will not be able to access the node’s information so it is excluded.