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

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// SPDX-FileCopyrightText: Copyright (c) 2020-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
  2// SPDX-License-Identifier: LicenseRef-NvidiaProprietary
  3//
  4// NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
  5// property and proprietary rights in and to this material, related
  6// documentation and any modifications thereto. Any use, reproduction,
  7// disclosure or distribution of this material and related documentation
  8// without an express license agreement from NVIDIA CORPORATION or
  9// its affiliates is strictly prohibited.
 10#include <OgnTutorialABIDatabase.h>
 11using omni::graph::core::Token;
 12
 13#include <string>
 14#include <unordered_map>
 15
 16// Set this value true to enable messages when the ABI overrides are called
 17const bool debug_abi{ true };
 18#define DEBUG_ABI                                                                                                      \
 19    if (debug_abi)                                                                                                     \
 20    CARB_LOG_INFO
 21
 22// In most cases the generated compute method is all that a node will need to implement. If for
 23// some reason the node wants to access the omni::graph::core::INodeType ABI directly and override
 24// any of its behavior it can. This set of functions shows how every method in the ABI might be
 25// overridden.
 26//
 27// Note that certain ABI functions, though listed, are not implemented as a proper implementation would be more
 28// complicated than warranted for this simple example. All are rarely overridden; in fact no known cases exist.
 29class OgnTutorialABI
 30{
 31    static std::unordered_map<std::string, std::string> s_metadata; // Alternative metadata implementation
 32public:
 33    // ----------------------------------------------------------------------
 34    // Almost always overridden indirectly
 35    //
 36    // ABI compute method takes the interface definitions of both the evaluation context and the node.
 37    // When using a .ogn generated class this function will be overridden by the generated code and the
 38    // node will implement the generated version of this method "bool compute(OgnTutorialABIDatabase&)".
 39    // Overriding this method directly, while possible, does not add any extra capabilities as the two
 40    // parameters are also available through the database (contextObj = db.abi_context(), nodeObj = db.abi_node())
 41    static bool compute(const GraphContextObj& contextObj, const NodeObj& nodeObj)
 42    {
 43        DEBUG_ABI("Computing the ABI node");
 44
 45        // The computation still runs the same as for the standard compute method by using the more
 46        // complex ABI-based access patterns. First get the ABI-compliant structures needed.
 47        NodeContextHandle nodeHandle = nodeObj.nodeContextHandle;
 48
 49        // Use the multiple-input template to access a pointer to the input boolean value
 50        auto inputValueAttr = getAttributesR<ConstAttributeDataHandle>(
 51            contextObj, nodeHandle, std::make_tuple(Token("inputs:namespace:a_bool")), kAccordingToContextIndex);
 52        const bool* inputValue{ nullptr };
 53        std::tie(inputValue) = getDataR<bool*>(contextObj, inputValueAttr);
 54
 55        // Use the single-output template to access a pointer to the output boolean value
 56        auto outputValueAttr =
 57            getAttributeW(contextObj, nodeHandle, Token("outputs:namespace:a_bool"), kAccordingToContextIndex);
 58        bool* outputValue = getDataW<bool>(contextObj, outputValueAttr);
 59
 60        // Since the generated code isn't in control you are responsible for your own error checking
 61        if (!inputValue)
 62        {
 63            // LCOV_EXCL_START : This tags a section of code for removal from the code coverage runs because
 64            //                   the condition it checks cannot be easily reproduced in normal operation.
 65            // Not having access to the generated database class with its error interface we have to resort
 66            // to using the ABI for error logging.
 67            nodeObj.iNode->logComputeMessageOnInstance(nodeObj, kAccordingToContextIndex,
 68                                                       omni::graph::core::ogn::Severity::eError,
 69                                                       "Failed compute: No input attribute");
 70            return false;
 71            // LCOV_EXCL_STOP
 72        }
 73        if (!outputValue)
 74        {
 75            // LCOV_EXCL_START
 76            nodeObj.iNode->logComputeMessageOnInstance(nodeObj, kAccordingToContextIndex,
 77                                                       omni::graph::core::ogn::Severity::eError,
 78                                                       "Failed compute: No output attribute");
 79            return false;
 80            // LCOV_EXCL_STOP
 81        }
 82
 83        // The actual computation of the node
 84        *outputValue = !*inputValue;
 85
 86        return true;
 87    }
 88
 89    // ----------------------------------------------------------------------
 90    // Rarely overridden
 91    //
 92    // These will almost never be overridden, specifically because the low level implementation lives in
 93    // the omni.graph.core extension and is not exposed through the ABI so it would be very difficult
 94    // for a node to be able to do the right thing when this function is called.
 95    // static void addInput
 96    // static void addOutput
 97    // static void addState
 98
 99    // ----------------------------------------------------------------------
100    // Rarely overridden
101    //
102    // This should almost never be overridden as the auto-generated code will handle the name.
103    // This particular override is used to bypass the unique naming feature of the .ogn name, where only names
104    // with a namespace separator (".") do not have the extension name added as a prefix to the unique name.
105    // This should only done for backward compatibility, and only until the node type name versioning is available.
106    // Note that when you do this you will still be able to create a node using the generated node type name, as
107    // that is required in order for the automated testing to work. The name returned here can be thought of as an
108    // alias for the underlying name.
109    static const char* getNodeType()
110    {
111        DEBUG_ABI("ABI override of getNodeType");
112        static const char* _nodeType{ "OmniGraphABI" };
113        return _nodeType;
114    }
115
116    // ----------------------------------------------------------------------
117    // Occasionally overridden
118    //
119    // When a node is created this will be called
120    static void initialize(const GraphContextObj&, const NodeObj&)
121    {
122        DEBUG_ABI("ABI override of initialize");
123        // There is no default behavior on initialize so nothing else is needed for this tutorial to function
124    }
125
126    // ----------------------------------------------------------------------
127    // Rarely overridden
128    //
129    // This method might be overridden to set up initial conditions when a node type is registered, or
130    // to replace initialization if the auto-generated version has some problem.
131    static void initializeType(const NodeTypeObj&)
132    {
133        DEBUG_ABI("ABI override of initializeType");
134        // The generated initializeType will always be called so nothing needs to happen here
135    }
136
137    // ----------------------------------------------------------------------
138    // Occasionally overridden
139    //
140    // This is called while registering a node type. It is used to initialize tasks that can be used for
141    // making compute more efficient by using Realm events.
142    static void registerTasks()
143    {
144        DEBUG_ABI("ABI override of registerTasks");
145    }
146
147    // ----------------------------------------------------------------------
148    // Occasionally overridden
149    //
150    // After a node is removed it will get a release call where anything set up in initialize() can be torn down
151    static void release(const NodeObj&)
152    {
153        DEBUG_ABI("ABI override of release");
154        // There is no default behavior on release so nothing else is needed for this tutorial to function
155    }
156
157    // ----------------------------------------------------------------------
158    // Occasionally overridden
159    //
160    // This is something you do want to override when you have more than version of your node.
161    // In it you would translate attribute information from older versions into the current one.
162    static bool updateNodeVersion(const GraphContextObj&, const NodeObj&, int oldVersion, int newVersion)
163    {
164        DEBUG_ABI("ABI override of updateNodeVersion from %d to %d", oldVersion, newVersion);
165        // There is no default behavior on updateNodeVersion so nothing else is needed for this tutorial to function
166        return oldVersion < newVersion;
167    }
168
169    // ----------------------------------------------------------------------
170    // Occasionally overridden
171    //
172    // When there is a connection change to this node which results in an extended type attribute being automatically
173    // resolved, this callback gives the node a change to resolve other extended type attributes. For example a generic
174    // 'Increment' node can resolve its output to an int only after its input has been resolved to an int. Attribute
175    // types are resolved using IAttribute::setResolvedType() or the utility functions such as
176    // INode::resolvePartiallyCoupledAttributes()
177    static void onConnectionTypeResolve(const NodeObj& nodeObj)
178    {
179        DEBUG_ABI("ABI override of onConnectionTypeResolve()");
180        // There is no default behavior on onConnectionTypeResolve so nothing else is needed for this tutorial to
181        // function
182    }
183
184    // ----------------------------------------------------------------------
185    // Rarely overridden
186    //
187    // You may want to provide an alternative method of metadata storage.
188    // This method can be used to intercept requests for metadata to provide those alternatives.
189    static size_t getAllMetadata(const NodeTypeObj& nodeType, const char** keyBuf, const char** valueBuf, size_t bufSize)
190    {
191        DEBUG_ABI("ABI override of getAllMetadata(%zu)", bufSize);
192        if (s_metadata.size() > bufSize)
193        {
194            CARB_LOG_ERROR("Not enough space for metadata - needed %zu, got %zu", s_metadata.size(), bufSize);
195            return 0;
196        }
197        size_t index = 0;
198        for (auto& metadata : s_metadata)
199        {
200            keyBuf[index] = metadata.first.c_str();
201            valueBuf[index] = metadata.second.c_str();
202            ++index;
203        }
204        return s_metadata.size();
205    }
206
207    // ----------------------------------------------------------------------
208    // Rarely overridden
209    //
210    // You may want to provide an alternative method of metadata storage.
211    // This method can be used to intercept requests for metadata to provide those alternatives.
212    static const char* getMetadata(const NodeTypeObj& nodeType, const char* key)
213    {
214        DEBUG_ABI("ABI override of getMetadata('%s')", key);
215        auto found = s_metadata.find(std::string(key));
216        if (found != s_metadata.end())
217        {
218            return (*found).second.c_str();
219        }
220        return nullptr;
221    }
222
223    // ----------------------------------------------------------------------
224    // Rarely overridden
225    //
226    // You may want to provide an alternative method of metadata storage.
227    // This method can be used to intercept requests for the number of metadata elements to provide those alternatives.
228    static size_t getMetadataCount(const NodeTypeObj& nodeType)
229    {
230        DEBUG_ABI("ABI override of getMetadataCount()");
231        return s_metadata.size();
232    }
233
234    // ----------------------------------------------------------------------
235    // Rarely overridden
236    //
237    // You may want to provide an alternative method of metadata storage.
238    // This method can be used to intercept requests to set metadata and use those alternatives.
239    static void setMetadata(const NodeTypeObj& nodeType, const char* key, const char* value)
240    {
241        DEBUG_ABI("ABI override of setMetadata('%s' = '%s')", key, value);
242        s_metadata[std::string(key)] = std::string(value);
243    }
244
245    // ----------------------------------------------------------------------
246    // Rarely overridden
247    //
248    // Subnode types are used when a single node type is shared among a set of other nodes, e.g. the way that
249    // PythonNode is the node type for all nodes implemented in Python, while the subNodeType is the actual node type.
250    // Overriding these functions let you implement different methods of managing those relationships.
251    //
252    static void addSubNodeType(const NodeTypeObj& nodeType, const char* subNodeTypeName, const NodeTypeObj& subNodeType)
253    {
254        DEBUG_ABI("ABI override of addSubNodeType('%s')", subNodeTypeName);
255    }
256
257    // ----------------------------------------------------------------------
258    // Rarely overridden
259    //
260    // Subnode types are used when a single node type is shared among a set of other nodes, e.g. the way that
261    // PythonNode is the node type for all nodes implemented in Python, while the subNodeType is the actual node type.
262    // Overriding these functions let you implement different methods of managing those relationships.
263    //
264    static NodeTypeObj getSubNodeType(const NodeTypeObj& nodeType, const char* subNodeTypeName)
265    {
266        DEBUG_ABI("ABI override of getSubNodeType('%s')", subNodeTypeName);
267        return NodeTypeObj{ nullptr, kInvalidNodeTypeHandle };
268    }
269
270    // ----------------------------------------------------------------------
271    // Rarely overridden
272    //
273    // Subnode types are used when a single node type is shared among a set of other nodes, e.g. the way that
274    // PythonNode is the node type for all nodes implemented in Python, while the subNodeType is the actual node type.
275    // Overriding these functions let you implement different methods of managing those relationships.
276    //
277    static NodeTypeObj createNodeType(const char* nodeTypeName, int version)
278    {
279        DEBUG_ABI("ABI override of createNodeType('%s')", nodeTypeName);
280        return NodeTypeObj{ nullptr, kInvalidNodeTypeHandle };
281    }
282};
283std::unordered_map<std::string, std::string> OgnTutorialABI::s_metadata;
284
285// ============================================================
286// The magic for recognizing and using the overridden ABI code is in here. The generated interface
287// is still available, as seen in the initializeType implementation, although it's not required.
288REGISTER_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.