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()