Tutorial 18 - Node With Internal State

This node illustrates how you can use internal state information, so long as you inform OmniGraph that you are doing so in order for it to make more intelligent execution scheduling decisions.

The advantage of using internal state data rather than state attributes is that the data can be in any structure you choose, not just those supported by OmniGraph. The disadvantage is that being opaque, none of the generic UI will be able to show information about that data.

An internal state can be associated with every graph instance for that node instance (db.perInstanceState), but a unique state shared by all graph instances (for that node instance) can be used as well (db.sharedState).

Notes to the reader: - A “node instance” refers to each individual node of a given type used in a graph. For example, you can have many instances of the node “Add” in a given graph. Each copy of this “Add” node is a node instance. - A “graph instance” refers to the data associated to a prim when this graph is applied to it (prim known as the “graph target”)

OgnTutorialState.ogn

The .ogn file containing the implementation of a node named “omni.graph.tutorials.State”. Unlike Python nodes with internal state the C++ nodes do not require and empty “state” section as the presence of state information is inferred from the data members in the node implementation class (i.e. mIncrementValue in this node).

 1{
 2    "State" : {
 3        "version": 1,
 4        "categories": "tutorials",
 5        "scheduling": ["threadsafe"],
 6        "description": [
 7            "This is a tutorial node. It makes use of internal state information",
 8            "to continuously increment an output."
 9        ],
10        "metadata":
11        {
12           "uiName": "Tutorial Node: Internal States"
13        },
14        "inputs": {
15            "overrideValue": {
16                "type": "int64",
17                "description": "Value to use instead of the monotonically increasing internal one when 'override' is true",
18                "default": 0,
19                "metadata": {
20                    "uiName": "Override Value"
21                }
22            },
23            "override": {
24                "type": "bool",
25                "description": "When true get the output from the overrideValue, otherwise use the internal value",
26                "default": false,
27                "metadata": {
28                    "uiName": "Enable Override"
29                }
30            },
31            "shared": {
32                "type": "bool",
33                "description": "Whether to use the state shared by all graph instances for this node, or a per graph-instance state",
34                "default": false,
35                "metadata": {
36                    "uiName": "Enable using a shared state amongst graph instances"
37                }
38            }
39        },
40        "outputs": {
41            "monotonic": {
42                "type": "int64",
43                "description": "Monotonically increasing output, set by internal state information",
44                "default": 0,
45                "metadata": {
46                    "uiName": "State-Based Output"
47                }
48            }
49        },
50        "tests": [
51            { "inputs:overrideValue": 555, "inputs:override": true, "outputs:monotonic": 555 }
52        ],
53        "$tests": "State tests are better done by a script that can control how many times a node executes."
54    }
55}

OgnTutorialState.cpp

The .cpp file contains the compute method and the internal state information used to run the algorithm.

By adding non-static class members to your node OmniGraph will know to instantiate a unique instance of your node for every evaluation context, letting you use those members as state data. The data in the node will be invisible to OmniGraph as a whole and will be persistent between evaluations of the node. Please note that using the node class itself as an internal state is recommended, but not mandatory. An internal state can be an instance of any C++ class of your choosing. This can be particularly interesting when using the sharedState as well as the perInstanceState, so both can be different C++ classes.

  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 <OgnTutorialStateDatabase.h>
 11#include <atomic>
 12
 13// Implementation of a C++ OmniGraph node that uses internal state information to compute outputs.
 14class OgnTutorialState
 15{
 16    // ------------------------------------------------------------
 17    // We can use the node class (or another type) to hold some state members, that can be attached to every instance
 18    // of that nodetype in a graph (ie. each time a copy of this node is added to a graph, also called a "node
 19    // instance") If the graph itself is instantiated (some graph targets have been added), every "graph instance" will
 20    // have its own state for this "node instance". An object shared amongst all graph instances can be associated to
 21    // each node instance, and accessed through the database class member "db.sharedState<>()" Another object can be
 22    // associated to each graph instance (for each node instance), and accessed through the database class member
 23    // "db.perInstanceState<>()"
 24
 25    // Start all nodes with a monotonic increment value of 0
 26    size_t mIncrementValue{ 0 };
 27
 28    // ------------------------------------------------------------
 29    // You can also define node-type static data, although you are responsible for dealing with any
 30    // thread-safety issues that may arise from accessing it from multiple threads (or multiple hardware)
 31    // at the same time. In this case it is a single value that is read and incremented so an atomic
 32    // variable is sufficient. In real applications this would be a complex structure, potentially keyed off
 33    // of combinations of inputs or real time information, requiring more stringent locking.
 34    //
 35    // This value increases for each node and indicates the value from which a node's own internal state value
 36    // increments. e.g. the first instance of this node type will start its state value at 1, the second instance at 2,
 37    // and so on...
 38    static std::atomic<size_t> sStartingValue;
 39
 40public:
 41    // You can add some per-instance initialization/release code in your constructor/destructor
 42    // The initInstance and releaseInstance callbacks can be implemented as well if some more demanding
 43    // setup/shutdown work is required.
 44    OgnTutorialState()
 45    {
 46    }
 47    ~OgnTutorialState()
 48    {
 49    }
 50
 51    // Helper function to update the node's internal state based on the previous values and the per-class state.
 52    // You could also do this in-place in the compute() function; pulling it out here makes the state manipulation
 53    // more explicit.
 54    void updateState()
 55    {
 56        mIncrementValue += 1;
 57    }
 58
 59    // When this method is implemented by the node class, it gets called by the framework
 60    //  whenever an instance is added to the graph
 61    static void initInstance(NodeObj const& node, GraphInstanceID instanceId);
 62
 63    // When this method is implemented by the node class, it gets called by the framework
 64    //  whenever an instance is removed from the graph
 65    static void releaseInstance(NodeObj const& node, GraphInstanceID instanceId);
 66
 67    // When this method is implemented by the node class, it gets called by the framework
 68    //  before the node gets removed from the graph
 69    static void release(NodeObj const& node);
 70
 71public:
 72    // Compute the output based on inputs and internal state
 73    static bool compute(OgnTutorialStateDatabase& db);
 74};
 75
 76//////////////////////////////////////////////////////////////////////////
 77// The shared state can be another object
 78class OgnTutorialSharedState : public OgnTutorialState
 79{
 80public:
 81    // Adds a member to the shared state to track the number of live initiated instances
 82    size_t mInstanceCount{ 0 };
 83};
 84
 85
 86//////////////////////////////////////////////////////////////////////////
 87/// IMPLEMENTATIONS
 88
 89std::atomic<size_t> OgnTutorialState::sStartingValue{ 0 };
 90
 91//--------------------------------------------------------------------------------------
 92void OgnTutorialState::initInstance(NodeObj const& node, GraphInstanceID instanceId)
 93{
 94    OgnTutorialState& state = OgnTutorialStateDatabase::sPerInstanceState<OgnTutorialState>(node, instanceId);
 95    state.mIncrementValue = sStartingValue;
 96    sStartingValue += 100;
 97
 98    OgnTutorialSharedState& sharedState = OgnTutorialStateDatabase::sSharedState<OgnTutorialSharedState>(node);
 99    sharedState.mInstanceCount++;
100}
101
102//--------------------------------------------------------------------------------------
103void OgnTutorialState::releaseInstance(NodeObj const& node, GraphInstanceID instanceId)
104{
105    OgnTutorialSharedState& sharedState = OgnTutorialStateDatabase::sSharedState<OgnTutorialSharedState>(node);
106    sharedState.mInstanceCount--;
107}
108
109//--------------------------------------------------------------------------------------
110void OgnTutorialState::release(NodeObj const& node)
111{
112    OgnTutorialSharedState& sharedState = OgnTutorialStateDatabase::sSharedState<OgnTutorialSharedState>(node);
113    if (sharedState.mInstanceCount != 0)
114    {
115        throw std::runtime_error("Releasing the node while some instances are still alive");
116    }
117}
118
119//--------------------------------------------------------------------------------------
120bool OgnTutorialState::compute(OgnTutorialStateDatabase& db)
121{
122    // This illustrates how internal state and inputs can be used in conjunction. The inputs can be used
123    // to divert to a different computation path.
124    if (db.inputs.override())
125    {
126        db.outputs.monotonic() = db.inputs.overrideValue();
127    }
128    else
129    {
130        // OmniGraph ensures that the database contains the correct internal state information for the node
131        // being evaluated. Beyond that it has no knowledge of the data within that state.
132        // This node can access either the object shared by all graph instance ("db.sharedState"),
133        //   or a distinct one that is allocated per graph instance
134        OgnTutorialState& state =
135            db.inputs.shared() ? db.sharedState<OgnTutorialSharedState>() : db.perInstanceState<OgnTutorialState>();
136        db.outputs.monotonic() = state.mIncrementValue;
137
138        // Update the node's internal state data for the next evaluation.
139        state.updateState();
140    }
141
142    return true;
143}
144
145REGISTER_OGN_NODE()